add docx export support to workspace-docs blueprint - #439
add docx export support to workspace-docs blueprint#439maxwellpeterson wants to merge 3 commits into
Conversation
84aa942 to
6cdd38c
Compare
Preview:
|
| } | ||
| }, | ||
| }); | ||
| const output = rewriter.transform(new Response(html, {headers: {"content-type": "text/html; charset=utf-8"}})); |
There was a problem hiding this comment.
[P1] Keep accepted data URLs below the parser ceiling
workerd configures the lol-html parser behind HTMLRewriter with a hard 3 MiB memory limit. A start tag containing a 3-8 MiB base64 src has to be buffered before this handler runs, so transform() fails even though DOCX_LIMITS.imageEncodedBytes explicitly accepts 8 MiB and persisted blocks accept roughly 10 MiB. A high-entropy image from the client can hit this range. Enforce a pre-parser limit below the runtime ceiling or avoid HTMLRewriter for these large attributes so inputs within the declared limits can actually export.
| stack.at(-1).children.push(node); | ||
| if (!VOID_TAGS.has(tag)) { | ||
| stack.push(node); | ||
| element.onEndTag(() => { |
There was a problem hiding this comment.
[P2] Do not register end handlers on self-closing foreign elements
VOID_TAGS only describes HTML void elements, but valid foreign markup can also be self-closing. For example, <p>before</p><svg/><p>after</p> reaches this branch for svg; lol-html reports that the element cannot have content when onEndTag() is called, failure is set, and the entire export rejects even though SVG is supposed to be ignored. Self-closing descendants such as <svg><path/></svg> fail the same way. Ignore foreign subtrees without installing unsupported end-tag handlers.
| } | ||
|
|
||
| function decodeHtmlEntities(value) { | ||
| return String(value).replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi, (match, entity) => { |
There was a problem hiding this comment.
[P2] Decode the full HTML named-reference set
lol-html passes text and attribute entities through as-is, but this decoder recognizes only six named references. Standard document HTML such as ©, —, or … remains unchanged here and is then escaped by xmlText, so Word displays the literal entity spelling rather than the intended character. This affects legacy and programmatically populated documents that have not first round-tripped through the browser. Use a complete HTML named-reference decoder.
|
|
||
| function gifDimensions(bytes) { | ||
| if (bytes.length < 22 || !["GIF87a", "GIF89a"].includes(ascii(bytes, 0, 6)) || | ||
| bytes.at(-1) !== 0x3b || !bytes.subarray(10, -1).includes(0x2c)) return null; |
There was a problem hiding this comment.
[P1] Validate GIF frame dimensions before embedding
The logical-screen width and height are not bounds on GIF image descriptors. This check only searches for any 0x2c, so a file can advertise a 1x1 logical screen while a frame descriptor declares 65,535x65,535 pixels; it then bypasses both imageDimension and imagePixels and is embedded for the Office decoder to process. Parse each image descriptor and enforce the per-axis/pixel limits on its frame dimensions (and animation aggregate), rather than trusting only bytes 6-9.
| const itemFormat = deriveFormat(format, item); | ||
| const itemParagraph = deriveParagraph(paragraphFormat, item, itemFormat); | ||
| const paragraph = builder.paragraph({...paragraphOptions("Normal", itemParagraph), list: {numId, level}}); | ||
| for (const child of item.children) { |
There was a problem hiding this comment.
[P2] Preserve nested-list child order
This two-pass conversion emits every non-list child before every nested list. Thus <li>before<ul><li>nested</li></ul>after</li> becomes parent text beforeafter followed by the nested list, changing the document meaning. Process the item children in source order, ending/resuming the parent paragraph around nested lists as needed.
| if (node.tag === "h1") return "Heading1"; | ||
| if (node.tag === "h2") return "Heading2"; | ||
| if (["h3", "h4", "h5", "h6"].includes(node.tag)) return "Heading3"; | ||
| if (node.tag === "blockquote") return "Quote"; |
There was a problem hiding this comment.
[P2] Distinguish toolbar indentation from quotations
The client toolbar uses Blink execCommand("indent"), which commonly persists normal indentation as <blockquote style="margin: 0 0 0 40px; border: none; padding: 0px;">.... Every such wrapper is mapped to Quote here, while deriveParagraph() also ignores the margin shorthand. Consequently Increase Indent turns ordinary text into a colored, bordered quotation and loses its requested indentation in Word. Detect the editor indentation wrapper and map its margin to paragraph indentation instead.
| function decodedImage(source, totals) { | ||
| const comma = source.indexOf(","); | ||
| if (comma < 0) return null; | ||
| const metadata = /^data:(image\/(?:png|jpeg|gif|webp));base64$/i.exec(source.slice(0, comma)); |
There was a problem hiding this comment.
[P2] Accept the JPEG spelling supported by the editor
client.js:isSafeImageDataUrl() explicitly accepts data:image/jpg;base64,..., persists it, and renders it, but this regex accepts only image/jpeg. Exporting that supported client content therefore silently replaces the picture with alt text or [Image unavailable]. Accept image/jpg here and normalize it to the JPEG content type/extension.
|
Submitted 7 actionable inline findings. The targeted 20-test DOCX suite passes. |
|
@maxwellpeterson Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
|
Findings
|
|
PR #439 adds DOCX export to the workspace-docs blueprint, with a preview deployment available. Bonk review found 11 actionable issues:
The targeted 20-test DOCX suite passes. One later Bonk workflow failed; the thread does not confirm that the findings have been resolved. |
|
Findings
|
update built-in docs blueprint to support docx export