Skip to content

add pptx export suppport to workspace-slides blueprint - #434

Open
maxwellpeterson wants to merge 15 commits into
mainfrom
mpeterson/pptx-export
Open

add pptx export suppport to workspace-slides blueprint#434
maxwellpeterson wants to merge 15 commits into
mainfrom
mpeterson/pptx-export

Conversation

@maxwellpeterson

Copy link
Copy Markdown
Member

update built-in slides blueprint to support pptx export

@github-actions github-actions Bot added the kernel Changes to the Workshop kernel label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Preview: pr434-mpeterson-pptx-export

https://pr434-mpeterson-pptx-export-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

@ask-bonk

ask-bonk Bot commented Sep 3, 2026

Copy link
Copy Markdown

@maxwellpeterson Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 3, 2026

Copy link
Copy Markdown

Posted 2 actionable inline findings.

github run

…atim

- Replace the slides ZIP writer with the workspace-sheets copy and adapt
  deckToPptx to its string/stream entry contract; batch slide XML into
  64 KiB chunks like xlsx.js.
- Parse highlight terms incrementally (max 128 comma-delimited entries),
  bound search work and highlight run transitions per title and per deck
  before any stream is returned, and stream runs directly from a shared
  mark array instead of materializing segments.
- Drop the duplicated ZIP32 and determinism tests; add regressions for the
  reported attacks, boundaries, cross-line highlights, and bundled
  blueprint dependency wiring.
@ask-bonk

ask-bonk Bot commented Sep 8, 2026

Copy link
Copy Markdown

APIError: You do not have access to the organization tied to the API key.

github run

2 similar comments
@ask-bonk

ask-bonk Bot commented Sep 8, 2026

Copy link
Copy Markdown

APIError: You do not have access to the organization tied to the API key.

github run

@ask-bonk

ask-bonk Bot commented Sep 8, 2026

Copy link
Copy Markdown

APIError: You do not have access to the organization tied to the API key.

github run

@maxwellpeterson
maxwellpeterson marked this pull request as ready for review September 8, 2026 20:51

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

🔍 Devin Review: 1 flag

Not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Google Slides renders exported decks with loose paragraphs and wrapped
logo/section labels. Emit CSS line-height as exact spcPts (spcPct is
relative to Arial's ~115% line gap), size single-line boxes from Arial
advance widths (Google ignores wrap="none"), keep the accent dot at the
browser's 3px gap, and enlarge the bullet glyph to the browser's 6px dot.
@ask-bonk

ask-bonk Bot commented Sep 8, 2026

Copy link
Copy Markdown

APIError: You do not have access to the organization tied to the API key.

github run

2 similar comments
@ask-bonk

ask-bonk Bot commented Sep 8, 2026

Copy link
Copy Markdown

APIError: You do not have access to the organization tied to the API key.

github run

@ask-bonk

ask-bonk Bot commented Sep 8, 2026

Copy link
Copy Markdown

APIError: You do not have access to the organization tied to the API key.

github run

devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 8, 2026

Copy link
Copy Markdown
  • [P1] server.js:539 PowerPoint export calls mutating getDeck(). A documented deck saved without themeVersion is replaced and persisted as the starter deck during export. Normalize setDeck() or use a non-mutating snapshot.

  • [P2] pptx.js:770 Any 1200x12 SVG containing the three brand colors is treated as the canonical brand bar. Additional authored content, such as text, is silently replaced by a plain gradient. Match the canonical SVG exactly or use explicit metadata.

  • [P2] pptx.js:1062, pptx.js:1085 Empty card/box titles still reserve a full estimated line, unlike the browser’s zero-height empty title element. Body-only components are displaced or mis-centered. Treat empty titles as zero-height.

github run

…etDeck schema stamp

- estimateTextHeight wraps greedily at spaces with Arial metrics and the
  run's tracking; auto-height text shapes also use spAutoFit so a
  consumer's own wrapping can only grow the box, never clip.
- The brand-bar shortcut requires the SVG to be built only from
  svg/defs/linearGradient/stop/rect; authored content keeps the placeholder.
- Empty card/box titles occupy no height and emit no shape.
- setDeck stamps themeVersion so a deck written over the GADGET binding
  is not discarded by the next getDeck (client boot or export).
@maxwellpeterson

Copy link
Copy Markdown
Member Author

Re Bonk's three findings (addressed in 2ca67ed):

  • P1 getDeck() mutates during export — root cause was setDeck() persisting a caller-built deck without the themeVersion schema marker that getDeck() checks, so the next read (client boot or export) discarded it. setDeck() now stamps themeVersion: "workspace.1"; export still reads through getDeck() like every other consumer, and only a genuinely malformed store gets reseeded.
  • P2 brand-bar heuristicisBrandBar() additionally requires the SVG to contain only svg/defs/linearGradient/stop/rect elements; any authored content (text, paths) keeps the visible SVG placeholder. Test: a 1200×12 three-color SVG with a <text> child renders the placeholder, not a gradient.
  • P2 empty card/box titles — an empty title now has zero height and emits no shape; the body sits at padding + one flex gap, as in the browser. Tests assert the body offsets.

devin-ai-integration[bot]

This comment was marked as resolved.

Google Slides converts spcPts back into a percentage of the font's natural
line height, so exact points rendered ~15% loose there; a percentage divided
by Arial's 1.15em natural height is what both PowerPoint and Google honour.
@ask-bonk

ask-bonk Bot commented Sep 9, 2026

Copy link
Copy Markdown
  • [P1] pptx.js:77 normalizeXml() builds rope-heavy strings character-by-character. A 100 KB structured-clone deck containing 79 references to one 100,000-character block passes limits but OOMs under a 128 MB heap. Normalize by spans/chunks instead.
  • [P2] pptx.js:1106 Card text shapes are not clipped to the card. Long titles or short resized cards export text beyond the surface, unlike the browser’s overflow: hidden.
  • [P2] pptx.js:1146 Box-body newlines become hard PowerPoint breaks, while the browser collapses them under normal whitespace. Multiline content can therefore overflow only after export.
  • [P2] pptx.js:197 Only hex colors are parsed. Named or functional CSS colors accepted by the editor render correctly in-browser but become fallback colors or transparent in PowerPoint.

Targeted PPTX tests pass.

github run

…ce, card shrink-to-fit

- normalizeXml and the run escaper copy clean spans instead of concatenating
  characters, so decks that alias one large block many times no longer build
  ropes the size of the deck text.
- parseColor accepts rgb()/rgba() and the basic CSS named colors.
- Props the browser renders with white-space normal/nowrap (section label,
  logo, pill, card eyebrow/title, box title/body, arrow label) collapse line
  breaks to spaces instead of becoming hard breaks.
- Card text shapes emit normAutofit so consumers shrink text into the card,
  the nearest native equivalent of overflow: hidden.
@maxwellpeterson

Copy link
Copy Markdown
Member Author

Re Bonk's latest four (addressed in 9611593):

  • P1 normalizeXml ropesnormalizeXml() now copies clean spans and returns the input untouched when nothing needs changing; the run escaper (escapedText) likewise yields spans between &/</>. Test: 79 aliases of a 100 KB block (7.9 MB of text with escapes) export in the normal suite budget.
  • P2 card text not clipped — PresentationML has no text clipping. Card eyebrow/title/body now emit <a:normAutofit/> so consumers shrink text into the card surface; documented as the nearest native equivalent of overflow: hidden.
  • P2 box body newlines — props the browser renders with white-space: normal/nowrap (box title/body, card eyebrow/title, section label, logo, pill, arrow label) now collapse runs of space/tab/newline to one space before export; card body/title/subtitle/text keep hard breaks as they are pre-wrap in the browser.
  • P2 non-hex colorsparseColor accepts rgb()/rgba() (numeric or percentage channels, optional alpha) and the basic CSS named colors; other names still fall back to the component default rather than guessing.

devin-ai-integration[bot]

This comment was marked as resolved.

buSzPts is the bullet font size; Arial's black circle inks a 0.43em disc,
so a 4.8pt (6px) marker needs an 11.16pt bullet font.
devin-ai-integration[bot]

This comment was marked as resolved.

@Maximo-Guk Maximo-Guk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm! ( looks like svg's aren't working in the export though, is that expected ? )

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 9, 2026

Copy link
Copy Markdown
  • [P1] pptx.js:37-39,720 Aggregate limits allow four unique 16 MiB images, retaining roughly 64 MiB decoded plus 85 MiB base64, exceeding the Worker’s 128 MiB heap.
  • [P1] pptx.js:692-698 Source-cache lookup occurs after scanning the full data URL. Reusing one 4 MiB source across 10,000 blocks causes tens of gigabytes of regex scanning and exceeds export deadlines.
  • [P2] pptx.js:1120 Bullet items preserve internal whitespace in PowerPoint while the browser collapses it, changing wrapping and potentially clipping content.

Posted 6 additional actionable inline findings.

github run

SVG markup becomes an image/svg+xml media part referenced through Office's
svgBlip extension, byte for byte and deliberately unvalidated: the deck's
SVG is trusted as authored (accepted risk; validation at the ingestion
boundary is the follow-up). contain letterboxes the frame by the viewBox
aspect; stretch fills the block. No raster fallback: consumers without SVG
support show an empty frame. Media parts now carry their own extension/
mime/aspect so content types and contain math are shared with raster
images.
devin-ai-integration[bot]

This comment was marked as resolved.

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 9, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding.

github run

Consumers apply spcPct to the first line, so the line-height-1 wordmark's
baseline moved and the dot, placed by a fixed offset, landed below it.
Emit the wordmark at natural spacing with the box raised so its baseline
lands where the browser's does, and put the dot 1px above that baseline
after Chrome's trailing letter-spacing and the 3px gap.
devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 9, 2026

Copy link
Copy Markdown
  • [P1] pptx.js:687 svgAspect() has quadratic regex backtracking. A 100 KB malformed viewBox took 9.4 seconds; larger accepted values exceed Worker limits. Parse attributes linearly.

  • [P2] pptx.js:708-715 SVG detection accepts nested markup such as <div><svg>…</svg></div>, then embeds the entire invalid document. The browser renders the selected <svg> node; serialize that node instead.

  • [P2] pptx.js:726 SVGs uploaded through the normal Image control become “Unsupported or malformed image” placeholders, despite client.js:2391-2393 explicitly preserving SVG uploads.

  • [P2] pptx.js:1185-1191 Card eyebrows always reserve one 12px line. Wrapped eyebrows are shrink-fitted and titles are positioned too high compared with the browser’s natural wrapping.

  • [P2] pptx.js:1034-1042 Cover export omits the two filled paths and stroked curve from COVER_ORANGE_SVG, visibly simplifying every starter cover.

  • [P2] pptx.js:263 PowerPoint’s preset dash scales with stroke width, unlike the browser’s fixed 6 6 arrow pattern. Thick dashed arrows export with substantially different spacing.

Targeted PPTX tests pass: 27 tests.

github run

…hful layout

- svgAspect parsed attributes with nested quantifiers (quadratic on a long
  malformed viewBox); attributes are now read with a linear scan and split.
- prepareImageSource consults the source cache before scanning the value,
  so one large source referenced from thousands of blocks is read once;
  aggregate media budgets halved to fit the Worker heap alongside the deck.
- Wrapped SVG pastes embed the <svg> element the browser renders; SVG files
  uploaded through the image control (data:image/svg+xml) are embedded too.
- isBrandBar is structural: exactly the canonical elements and stop colors.
- Layout parity: blocks without w shrink-to-fit up to the slide edge instead
  of fixed defaults; bullet items collapse inner whitespace; card eyebrows
  measure their height; overfull boxes centre their stack; pills stay
  intrinsic; omitted arrow endpoints are 0 like an absent SVG attribute;
  a letterboxed image is not rounded; dashes are a fixed 6px via custDash;
  the gadgets mark is a point-up hexagon with the scaled stroke; an empty
  wordmark keeps the dot's 3px gap.
@maxwellpeterson

Copy link
Copy Markdown
Member Author

Bonk's two rounds addressed in 4b1169e:

  • P1 quadratic svgAspect — attribute values are now read with a linear scan (svgAttribute: \sname\s*=\s*"([^"]*)") and split; the 100 KB viewBox case runs in the normal test budget (asserted <500 ms).
  • P1 source cache after scanprepareImageSource checks bySource before touching the value; 1000 references to a 4 MB source decode once (asserted).
  • P1 heap — aggregate budgets halved (48 MiB encoded / 32 MiB decoded) since the deck's own strings stay alive alongside the decoded bytes.
  • P2s fixed: wrapped <div><svg> embeds the <svg> element; data:image/svg+xml uploads embed; bullet items collapse whitespace; card eyebrows measure their height; overfull boxes centre; pills stay intrinsic; omitted arrow endpoints are 0; blocks without w shrink-to-fit up to the slide edge; contain images aren't rounded; dashes are a fixed 6px via custDash; gadgets mark is a rotated point-up hexagon with the scaled stroke (QuickLook-verified).
  • Declined: cover sweeps — the freeform custGeom paths were removed on purpose in 48f957e because Google Slides rejected the package; the gradient-only cover stays.

devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 9, 2026

Copy link
Copy Markdown
  • [P1] pptx.js:944-947 isBrandBar() retains every tag match. A valid 999,999-character SVG containing 249,997 <g/> elements peaked near the Worker memory limit before streaming. Compare incrementally, stop after the eighth element, and cache by source.

  • [P2] pptx.js:307-312 Both gadgetsMark sizes emit fractional a:off coordinates such as 34023.5. DrawingML coordinates require integers, potentially triggering document repair. Round transformed offsets.

  • [P2] pptx.js:1267,1303 Missing card/box heights become fixed 260px/110px. Since h is optional, the browser instead produces content-driven height. Programmatically authored blocks export with substantial empty space.

  • [P2] pptx.js:721-734,1484 SVGs without a viewBox use authored width/height for letterboxing. The browser overwrites those attributes with 100%, so a 300×100 SVG in a square block fills a square viewport rather than being centered as a short strip.

  • [P2] pptx.js:408-412 Highlight matches are unioned rather than applied in browser order. For text: "foobar", highlight: "foo,foobar", the browser highlights only foo, while PowerPoint highlights all of foobar.

github run

svgElement sliced from the first opening tag to the last closing tag, so
sibling elements produced a media part with several roots. A linear scan
over svg open/close tags now tracks nesting (and quoted attributes) and
returns exactly the first element, as the browser's querySelector does.
devin-ai-integration[bot]

This comment was marked as resolved.

if (!bytes) return {placeholder: "Malformed image data"};
if (match[1] === "svg+xml") {
// The image control passes uploaded SVG files through verbatim (client.js fileToImageDataURL).
const element = svgElement(decoder.decode(bytes));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep SVG upload conversion within the Worker heap. Two distinct valid 16 MiB SVG data URLs pass the 48 MiB encoded / 32 MiB decoded aggregate limits, but on the second image this path retains the deck’s ~43 MiB of base64, the first 16 MiB media buffer, and another decoded buffer, JS string, and re-encoded buffer. In a direct reproduction, deckToPptx() raised RSS from 101 MiB to 253 MiB before streaming, so a 128 MiB Worker can OOM despite all limits passing. Preserve the original decoded bytes while locating the SVG element, or apply a substantially lower SVG-specific budget.

const bytes = decodeBase64(payload, decodedLength);
if (!bytes) return {placeholder: "Malformed image data"};
if (match[1] === "svg+xml") {
// The image control passes uploaded SVG files through verbatim (client.js fileToImageDataURL).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve valid non-UTF-8 SVG uploads. fileToImageDataURL() passes SVG files through verbatim, and XML permits encodings such as UTF-16, but the unconditional UTF-8 TextDecoder below turns a BOM-prefixed UTF-16 SVG into text with no recognizable <svg> and exports a Malformed image data placeholder. Detect the XML encoding or parse/extract directly from the original bytes.

}

function svgMedia(element, mediaState) {
return findOrAddMedia(encoder.encode(element.source), {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Make pasted SVG fragments valid standalone SVG media. For <svg viewBox="0 0 10 10"><rect/></svg>, the editor’s HTML parser creates an SVG-namespaced node and renders it, but this byte-for-byte encoding leaves the media root without xmlns="http://www.w3.org/2000/svg". Chromium rejects that standalone image/svg+xml, and PowerPoint can consequently show a blank frame. Serialize the selected node or inject the SVG namespace when it is absent.

// The value of `name="..."` in an element's opening tag, or null. A plain scan with no nested
// quantifiers: the tag is authored text and can be long, so parsing must stay linear.
function svgAttribute(rootTag, name) {
const match = new RegExp(String.raw`\s${name}\s*=\s*(?:"([^"]*)"|'([^']*)')`).exec(rootTag);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Skip quoted values while locating an SVG attribute. With valid markup <svg aria-label='chart viewBox = "0 0 200 100"' viewBox="0 0 100 100">…</svg>, this regex reads the attribute-like text inside aria-label, so a square SVG in a 400×400 contain block exports as 400×200 while the browser uses the real square viewBox. A linear opening-tag scanner should track quotes and match only actual attribute names.

if (!color || width === 0) return "<a:ln><a:noFill/></a:ln>";
let xml = `<a:ln w="${width}" cap="rnd">${solidFill(color, shapeOpacity)}`;
if (dashed) {
const dash = Math.round(6 / (width / PX_TO_LINE_EMU) * 100000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Bound the custom-dash percentages before writing OOXML. A persisted dashed arrow with width: 0.0001 rounds to a 1-EMU line and emits d="6096000000" sp="6096000000"; CT_DashStop uses ST_PositivePercentage, whose integer maximum is 2147483647. This produces schema-invalid DrawingML that consumers may repair or drop. Clamp the percentage or enforce a representable minimum dashed width.

@ask-bonk

ask-bonk Bot commented Sep 9, 2026

Copy link
Copy Markdown

Posted 5 actionable inline findings. Targeted PPTX tests pass: 30/30.

github run

pptx.js no longer knows the `logo` block. server.js exports
normalizeDeckForPptx(), a pure adapter that rewrites each logo into a
text block (wordmark) and an ellipse shape (accent dot) at the logo's
z-order position and previous geometry, using the renderer's new
measureText() export for the wordmark's width and baseline. Text blocks
honour letterSpacing in both renderers. Another slides blueprint can now
copy pptx.js byte for byte and write its own adapter.

Review findings:
- svgElement steps over comments, CDATA and <!/<? declarations, skips
  every tag's quoted attribute values, ignores stray end tags.
- tagAttribute is a linear attribute scanner, so attribute-like text in
  another value is never read (svgAttribute regex).
- A pasted root without xmlns gets the SVG namespace declaration.
- Uploaded SVG files are decoded by BOM / XML declaration (UTF-16,
  Latin-1, ...) and have a 4 MiB budget of their own.
- isBrandBar compares tags as found and shares the per-source cache.
- custDash percentages are clamped to the schema's integer maximum.
- The gadgets mark's rotated box rounds to integer EMU.
- Cards and boxes without w/h are sized to their content.
- SVG aspect comes from viewBox only, as the browser overrides
  width/height with 100%.
- Title highlights apply in term order, as the browser wraps them.
devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 10, 2026

Copy link
Copy Markdown
  • [P1] server.js:559-595 Logo normalization runs before exporter limits. Repeating one aliased 1 MB logo 1,000 times performs roughly 1 billion characters of preprocessing before text limits apply. Validate before normalization or expand logos during bounded preparation.

  • [P1] pptx.js:649-654 Later JPEG SOF markers overwrite earlier dimensions. A JPEG can declare an oversized first frame, then a small frame, bypassing pixel limits. Reject duplicate/out-of-order SOFs and validate dimensions immediately.

  • [P2] pptx.js:702-704 CRC32 collision buckets cause quadratic byte comparisons. Up to 256 equal-length, colliding images can trigger gigabytes of comparison work. Avoid cross-source deduplication or use collision-resistant hashing.

  • [P2] pptx.js:951-968 Uploaded SVGs without viewBox lose their intrinsic width/height aspect ratio. contain and cover consequently stretch them, unlike the browser <img> rendering.

  • [P2] pptx.js:280 PowerPoint’s small preset arrowhead is substantially smaller than the browser’s markerWidth="10" stroke-scaled marker. Ordinary arrows export with visibly different endpoints.

  • [P2] pptx.js:1818 Grid spacing is emitted as 78,028,800 EMU, about 85 inches. Enabling grid snapping makes object movement unusable; emit a normal spacing or omit it.

github run

Google Slides, Quick Look and older PowerPoint render an svgBlip as an
empty frame, and the exporter has no rasterizer for the PNG fallback such
a picture needs, so SVG blocks and SVG uploads become the visible "SVG
not included" placeholder again (the brand-bar gradient stays). The SVG
scanner, encoding detection and svgBlip picture path go with it.

Review findings:
- normalizeDeckForPptx stops measuring wordmarks once they alone exceed
  the renderer's total-text limit (exported as MAX_TOTAL_TEXT_LENGTH),
  so an aliased huge logo cannot buy unbounded work ahead of validation.
- A JPEG with a second SOF header is rejected instead of the later frame
  overwriting the dimensions the pixel limits were checked against.
- Media are deduplicated by source string only; the byte comparison
  behind CRC32 buckets is gone.
- Arrowheads use the largest preset (5x5), closest to the browser's 9x6
  stroke-scaled marker.
- viewProps grid spacing is PowerPoint's default 1/12 inch.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +758 to +761
let result = mediaState.bySource.get(markup);
if (!result) {
result = isBrandBar(markup) ? {brandBar: true} : {placeholder: "SVG not included"};
mediaState.bySource.set(markup, result);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Cross-type source cache corrupts blocks

When one string serves as image data and SVG markup, bySource returns the first block type's incompatible result. The later block becomes a placeholder or makes ZIP streaming fail.

Learn more

Image sources and SVG markup share mediaState.bySource, but their cached result shapes differ. prepareImageSource expects {media} or an image placeholder, while prepareSvgSource expects {brandBar} or an SVG placeholder. An image-first collision gives the SVG renderer {media}, so it passes undefined into text generation and the returned ZIP stream errors during consumption. An SVG-first collision gives the image renderer an SVG placeholder and silently omits valid image data.

Example: Use one valid PNG data URL as an image block's props.src and as a later SVG block's props.markup. The image caches {media}. The SVG reads that entry, reaches its placeholder path with no placeholder, and ZIP streaming fails instead of producing the deck.

Recommended fix: Keep separate image and SVG caches, or use collision-free typed keys. Preserve image deduplication and SVG recognition caching within each type.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

const BRAND_BAR_STOPS = ["#FF6633", "#F6821F", "#FBAD41"];

function isBrandBar(markup) {
const tags = /<([A-Za-z][\w:-]*)([^>]*)>/g;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Replace this regex with a monotonic scanner. "<a".repeat(n) makes [^>]* scan the remaining suffix and fail once for every <a, so runtime is quadratic even though no tag matches. Through deckToPptx(), 20,000 repetitions took 0.63 s and 40,000 took 2.69 s here; the accepted one-million-character markup limit extrapolates beyond the export Worker deadline. Malformed SVG entered via addBlock() should become a placeholder without consuming minutes of CPU.

return normalizeXml(text);
}

function sourceScalar(value) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Parse and bound numeric scalar strings during preparation. These strings are not charged to MAX_TOTAL_TEXT_LENGTH, and renderers repeatedly call trim()/Number() on them through numberOr() and isBold(). One shared title with weight = " ".repeat(300_000) aliased across the allowed 10,000 blocks took 13.2 s to consume; a roughly 1.2 MB scalar exceeds the export deadline while the deck still passes every text/block limit. Store the parsed bounded number once rather than retaining and rescanning the source string.

}
const match = IMAGE_DATA_URL.exec(value);
if (!match) {
boundedText(value, label, limits);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Recognize intentionally omitted SVG uploads before applying the generic text limit. The image control preserves SVG files verbatim, but a valid 740 KiB SVG becomes a 1,010,374-character base64 URL and this call rejects the entire deck as overlong instead of returning the documented SVG not included placeholder. Apply an explicit source/media bound, then short-circuit SVG data URLs before boundedText().

@ask-bonk

ask-bonk Bot commented Sep 10, 2026

Copy link
Copy Markdown

Posted 3 actionable inline findings.

github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kernel Changes to the Workshop kernel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants