@@ -34,6 +34,11 @@ const INLINE_EMPHASIS_PATTERN = /\*\*([^*]+)\*\*|\*([^*]+)\*/g
3434/** Characters XML 1.0 forbids in a text node. */
3535const INVALID_XML_CHARS = / [ \u0000 - \u0008 \u000B \u000C \u000E - \u001F ] / g
3636
37+ /** Removes the characters XML 1.0 forbids in a text node. */
38+ function stripInvalidXmlChars ( value : string ) : string {
39+ return value . replace ( INVALID_XML_CHARS , '' )
40+ }
41+
3742/**
3843 * Splits a line into bold / italic runs using the Markdown subset Sim supports:
3944 * `**bold**` and `*italic*`. Anything else is emitted verbatim, so unmatched
@@ -77,7 +82,12 @@ function parseInlineRuns(line: string): ContentRun[] {
7782function parseContentBlocks ( content : string ) : ContentBlock [ ] {
7883 const blocks : ContentBlock [ ] = [ ]
7984
80- for ( const rawLine of content . replace ( / \r \n ? / g, '\n' ) . split ( '\n' ) ) {
85+ // Stripped once here rather than per run: the `docx` package writes run text
86+ // into the XML verbatim, so a stray control character from an upstream block
87+ // would produce a package Word refuses to open. Tab, newline, and carriage
88+ // return are legal XML and are outside the class, so the split below is
89+ // unaffected.
90+ for ( const rawLine of stripInvalidXmlChars ( content ) . replace ( / \r \n ? / g, '\n' ) . split ( '\n' ) ) {
8191 const line = rawLine . trimEnd ( )
8292 if ( line . trim ( ) . length === 0 ) continue
8393
@@ -132,8 +142,7 @@ export async function buildDocxFromContent(content: string, title?: string): Pro
132142
133143/** Escapes text for an XML text node and drops characters XML 1.0 forbids. */
134144function escapeXmlText ( value : string ) : string {
135- return value
136- . replace ( INVALID_XML_CHARS , '' )
145+ return stripInvalidXmlChars ( value )
137146 . replace ( / & / g, '&' )
138147 . replace ( / < / g, '<' )
139148 . replace ( / > / g, '>' )
0 commit comments