Signal drop!
Relay (operand.online) is unreachable.
Usually, a dropped signal means an upgrade is happening. Hold on!
Sorry, no connección.
Hang in there while we get back on track
gram: docs
> ./packages/core/src/docx/serializer/documentSerializer.ts
/**
* Document Serializer - Serialize complete document.xml
*
* Converts Document objects back to valid document.xml OOXML format.
* Combines all content (paragraphs, tables) with section properties
* and proper namespace declarations.
*
* OOXML Reference:
* - Document root: w:document
* - Document body: w:body
* - Section properties: w:sectPr
*/
import type { Document, DocumentBody, BlockContent } from '../../types/document';
import { serializeParagraph } from './paragraphSerializer';
import { resetAutoIdCounter } from './runSerializer';
import { serializeTable } from './tableSerializer';
import { serializeBlockSdt } from './sdtSerializer';
import { serializeSectionProperties } from './sectionPropertiesSerializer';
// ============================================================================
// XML NAMESPACES
// ============================================================================
/**
* Standard OOXML namespaces for document.xml
*/
const NAMESPACES = {
wpc: 'http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas',
cx: 'http://schemas.microsoft.com/office/drawing/2014/chartex',
cx1: 'http://schemas.microsoft.com/office/drawing/2015/9/8/chartex',
cx2: 'http://schemas.microsoft.com/office/drawing/2015/10/21/chartex',
cx3: 'http://schemas.microsoft.com/office/drawing/2016/5/9/chartex',
cx4: 'http://schemas.microsoft.com/office/drawing/2016/5/10/chartex',
cx5: 'http://schemas.microsoft.com/office/drawing/2016/5/11/chartex',
cx6: 'http://schemas.microsoft.com/office/drawing/2016/5/12/chartex',
cx7: 'http://schemas.microsoft.com/office/drawing/2016/5/13/chartex',
cx8: 'http://schemas.microsoft.com/office/drawing/2016/5/14/chartex',
mc: 'http://schemas.openxmlformats.org/markup-compatibility/2006',
aink: 'http://schemas.microsoft.com/office/drawing/2016/ink',
am3d: 'http://schemas.microsoft.com/office/drawing/2017/model3d',
o: 'urn:schemas-microsoft-com:office:office',
oel: 'http://schemas.microsoft.com/office/2019/extlst',
r: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
m: 'http://schemas.openxmlformats.org/officeDocument/2006/math',
v: 'urn:schemas-microsoft-com:vml',
wp14: 'http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing',
wp: 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing',
w10: 'urn:schemas-microsoft-com:office:word',
w: 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
w14: 'http://schemas.microsoft.com/office/word/2010/wordml',
w15: 'http://schemas.microsoft.com/office/word/2012/wordml',
w16cex: 'http://schemas.microsoft.com/office/word/2018/wordml/cex',
w16cid: 'http://schemas.microsoft.com/office/word/2016/wordml/cid',
w16: 'http://schemas.microsoft.com/office/word/2018/wordml',
w16sdtdh: 'http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash',
w16se: 'http://schemas.microsoft.com/office/word/2015/wordml/symex',
wpg: 'http://schemas.microsoft.com/office/word/2010/wordprocessingGroup',
wpi: 'http://schemas.microsoft.com/office/word/2010/wordprocessingInk',
wne: 'http://schemas.microsoft.com/office/word/2006/wordml',
wps: 'http://schemas.microsoft.com/office/word/2010/wordprocessingShape',
};
/**
* Build namespace declaration string for document element
*/
function buildNamespaceDeclarations(): string {
// Minimal set of commonly used namespaces
const minimalNamespaces = {
wpc: NAMESPACES.wpc,
mc: NAMESPACES.mc,
o: NAMESPACES.o,
r: NAMESPACES.r,
m: NAMESPACES.m,
v: NAMESPACES.v,
wp14: NAMESPACES.wp14,
wp: NAMESPACES.wp,
w10: NAMESPACES.w10,
w: NAMESPACES.w,
w14: NAMESPACES.w14,
w15: NAMESPACES.w15,
// Modern Word (2016+) extension namespaces. Declared because verbatim
// passthrough (e.g. a captured w:sdtPr) can echo w16*/wne-prefixed
// children; an undeclared prefix is a fatal XML error that makes Word
// offer to repair the file.
w16se: NAMESPACES.w16se,
w16cid: NAMESPACES.w16cid,
w16: NAMESPACES.w16,
w16cex: NAMESPACES.w16cex,
w16sdtdh: NAMESPACES.w16sdtdh,
wne: NAMESPACES.wne,
wpg: NAMESPACES.wpg,
wps: NAMESPACES.wps,
};
return Object.entries(minimalNamespaces)
.map(([prefix, uri]) => `xmlns:${prefix}="${uri}"`)
.join(' ');
}
// ============================================================================
// CONTENT SERIALIZATION
// ============================================================================
/**
* Serialize a single block content item (paragraph, table, or block SDT).
*
* Exported so the footnote/endnote serializer can reuse the exact same
* block-level emission the document body uses — preserving tracked-change
* wrappers (`w:ins`/`w:del`), paragraph/run properties, fields, and tables
* inside note bodies instead of reimplementing a minimal serializer.
*/
export function serializeBlockContent(block: BlockContent): string {
if (block.type === 'paragraph') {
return serializeParagraph(block);
} else if (block.type === 'table') {
return serializeTable(block);
} else if (block.type === 'blockSdt') {
return serializeBlockSdt(block, serializeBlockContent);
}
return '';
}
/**
* Serialize document body content
*/
function serializeBodyContent(content: BlockContent[]): string {
return content.map((block) => serializeBlockContent(block)).join('');
}
// ============================================================================
// MAIN DOCUMENT SERIALIZATION
// ============================================================================
/**
* Serialize a DocumentBody to document.xml body content
*
* @param body - The document body to serialize
* @returns XML string for the body element (without body tags)
*/
export function serializeDocumentBody(body: DocumentBody): string {
const parts: string[] = [];
// Serialize all content blocks
parts.push(serializeBodyContent(body.content));
// Final section properties (at the end of body)
if (body.finalSectionProperties) {
parts.push(serializeSectionProperties(body.finalSectionProperties));
}
return parts.join('');
}
/**
* Serialize a complete Document to valid document.xml
*
* @param doc - The document to serialize
* @returns Complete XML string for document.xml
*/
export function serializeDocument(doc: Document): string {
// Reset auto-incrementing image/shape ID counter for this serialization pass
resetAutoIdCounter();
const parts: string[] = [];
// XML declaration
parts.push('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>');
// Document element with namespaces
const nsDecl = buildNamespaceDeclarations();
parts.push(`<w:document ${nsDecl} mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh wp14">`);
// Document body
parts.push('<w:body>');
parts.push(serializeDocumentBody(doc.package.document));
parts.push('</w:body>');
// Close document element
parts.push('</w:document>');
return parts.join('');
}
/**
* Serialize just the document body (useful for partial updates)
*
* @param body - The document body to serialize
* @returns XML string for the w:body element
*/
export function serializeDocumentBodyElement(body: DocumentBody): string {
return `<w:body>${serializeDocumentBody(body)}</w:body>`;
}
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Check if document has any content
*/
export function hasDocumentContent(doc: Document): boolean {
return doc.package.document.content.length > 0;
}
/**
* Check if document has sections
*/
export function hasDocumentSections(doc: Document): boolean {
return (doc.package.document.sections?.length ?? 0) > 0;
}
/**
* Check if document has section properties
*/
export function hasSectionProperties(doc: Document): boolean {
return doc.package.document.finalSectionProperties !== undefined;
}
/**
* Get document content count (paragraphs + tables)
*/
export function getDocumentContentCount(doc: Document): number {
return doc.package.document.content.length;
}
/**
* Get paragraph count in document
*/
export function getDocumentParagraphCount(doc: Document): number {
return doc.package.document.content.filter((b) => b.type === 'paragraph').length;
}
/**
* Get table count in document
*/
export function getDocumentTableCount(doc: Document): number {
return doc.package.document.content.filter((b) => b.type === 'table').length;
}
/**
* Get plain text from document (for comparison/debugging)
*/
export function getDocumentPlainText(doc: Document): string {
const texts: string[] = [];
for (const block of doc.package.document.content) {
if (block.type === 'paragraph') {
for (const content of block.content) {
if (content.type === 'run') {
for (const item of content.content) {
if (item.type === 'text') {
texts.push(item.text);
} else if (item.type === 'tab') {
texts.push('\t');
} else if (item.type === 'break') {
texts.push('\n');
}
}
}
}
texts.push('\n'); // Paragraph break
}
}
return texts.join('');
}
/**
* Create an empty document
*/
export function createEmptyDocument(): Document {
return {
package: {
document: {
content: [],
},
},
};
}
/**
* Create a simple document with text content
*/
export function createSimpleDocument(
paragraphs: Array<{ text: string; styleId?: string }>
): Document {
return {
package: {
document: {
content: paragraphs.map((p) => ({
type: 'paragraph' as const,
formatting: p.styleId ? { styleId: p.styleId } : undefined,
content: [
{
type: 'run' as const,
content: [{ type: 'text' as const, text: p.text }],
},
],
})),
},
},
};
}
export default serializeDocument;