Skip to content

Commit ab6026d

Browse files
committed
fix(viewport): 只扫描 style 块,避免大 base64 HTML 卡死
与 html2pptx 同步:parseFixedSlideHostSize 不再对整份 HTML 跑 CSS 规则正则,并补充大 data-URI 回归测试。
1 parent 97903c8 commit ab6026d

2 files changed

Lines changed: 67 additions & 18 deletions

File tree

src/utils/viewport.ts

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -151,26 +151,58 @@ export function parseDeckCssVariables(cssOrHtml: string): ViewportSize | null {
151151
}
152152

153153
/**
154-
* Read fixed `width`/`height` px on common slide host / root selectors.
154+
* Extract text content of `<style>` blocks from HTML.
155+
* Used so CSS rule scanners never run against markup / data-URI blobs
156+
* (those can be multi-MB lines without `{`/`}`, which makes `/[^{}@]+\{/` O(n²)).
155157
*/
156-
export function parseFixedSlideHostSize(cssOrHtml: string): ViewportSize | null {
157-
// Match CSS rule blocks; keep it simple (no nested @rules required for our cases).
158-
const ruleRe = /([^{}@]+)\{([^{}]+)\}/g;
158+
export function extractInlineStyleBlocks(html: string): string {
159+
const chunks: string[] = [];
160+
const styleRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
159161
let match: RegExpExecArray | null;
160-
while ((match = ruleRe.exec(cssOrHtml)) !== null) {
161-
const selectors = match[1]!;
162-
const body = match[2]!;
163-
if (!SLIDE_HOST_SELECTOR_RE.test(selectors)) continue;
162+
while ((match = styleRe.exec(html)) !== null) {
163+
const css = match[1];
164+
if (css) chunks.push(css);
165+
}
166+
return chunks.join('\n');
167+
}
164168

165-
const widthMatch = body.match(/(?:^|[;\s])width\s*:\s*([^;}\n]+)/i);
166-
const heightMatch = body.match(/(?:^|[;\s])height\s*:\s*([^;}\n]+)/i);
167-
if (!widthMatch || !heightMatch) continue;
168-
const width = parseCssPxLength(widthMatch[1]!);
169-
const height = parseCssPxLength(heightMatch[1]!);
170-
if (width == null || height == null) continue;
171-
// Ignore tiny decorative boxes accidentally matching a host selector.
172-
if (width < 320 || height < 180) continue;
173-
return { width, height };
169+
/**
170+
* CSS chunks safe to scan with the CSS-rule regex.
171+
* Prefer `<style>` contents when present; skip HTML markup (no style tags);
172+
* otherwise treat the whole string as CSS (linked stylesheets / pure CSS).
173+
*/
174+
function cssChunksForRuleScan(cssOrHtml: string): string[] {
175+
const inline = extractInlineStyleBlocks(cssOrHtml);
176+
if (inline) return [inline];
177+
// HTML without <style> — do not scan body / base64 blobs.
178+
if (/<[a-zA-Z!/?]/.test(cssOrHtml)) return [];
179+
return [cssOrHtml];
180+
}
181+
182+
/**
183+
* Read fixed `width`/`height` px on common slide host / root selectors.
184+
* Scans only CSS (inline `<style>` blocks or pure stylesheet text), never raw HTML.
185+
*/
186+
export function parseFixedSlideHostSize(cssOrHtml: string): ViewportSize | null {
187+
for (const css of cssChunksForRuleScan(cssOrHtml)) {
188+
// Match CSS rule blocks; keep it simple (no nested @rules required for our cases).
189+
const ruleRe = /([^{}@]+)\{([^{}]+)\}/g;
190+
let match: RegExpExecArray | null;
191+
while ((match = ruleRe.exec(css)) !== null) {
192+
const selectors = match[1]!;
193+
const body = match[2]!;
194+
if (!SLIDE_HOST_SELECTOR_RE.test(selectors)) continue;
195+
196+
const widthMatch = body.match(/(?:^|[;\s])width\s*:\s*([^;}\n]+)/i);
197+
const heightMatch = body.match(/(?:^|[;\s])height\s*:\s*([^;}\n]+)/i);
198+
if (!widthMatch || !heightMatch) continue;
199+
const width = parseCssPxLength(widthMatch[1]!);
200+
const height = parseCssPxLength(heightMatch[1]!);
201+
if (width == null || height == null) continue;
202+
// Ignore tiny decorative boxes accidentally matching a host selector.
203+
if (width < 320 || height < 180) continue;
204+
return { width, height };
205+
}
174206
}
175207
return null;
176208
}
@@ -241,17 +273,19 @@ function detectFromCssSources(...sources: string[]): ViewportSize | null {
241273
* Detect declared slide size from HTML source.
242274
* Order: deck-size meta → CSS size vars → viewport meta → stage JSON → fixed host px.
243275
* When `baseDir` is set, also reads local linked stylesheets for CSS signals.
276+
* CSS detectors only see `<style>` contents + linked stylesheets (not HTML body / data URIs).
244277
*/
245278
export function detectViewportFromHtml(
246279
html: string,
247280
options: { baseDir?: string } = {}
248281
): ViewportSize | null {
249282
const linkedCss =
250283
options.baseDir != null ? readLocalStylesheets(html, options.baseDir) : '';
284+
const inlineCss = extractInlineStyleBlocks(html);
251285

252286
return (
253287
parseDeckSizeMeta(html) ??
254-
detectFromCssSources(html, linkedCss) ??
288+
detectFromCssSources(inlineCss, linkedCss) ??
255289
parseViewportMeta(html) ??
256290
parseStageJson(html)
257291
);

test/unit/viewport-detect.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ describe('parseFixedSlideHostSize', () => {
108108
});
109109
});
110110

111+
it('reads host size from <style> inside HTML', () => {
112+
const html = `<!doctype html><style>.slide-container { width: 1280px; height: 720px; }</style><img src="data:image/png;base64,${'A'.repeat(200_000)}">`;
113+
assert.deepEqual(parseFixedSlideHostSize(html), {
114+
width: 1280,
115+
height: 720,
116+
});
117+
});
118+
119+
it('does not hang on large data-URI HTML without style host rules', () => {
120+
const html = `<!doctype html><body><img src="data:image/png;base64,${'A'.repeat(500_000)}"></body>`;
121+
const t0 = Date.now();
122+
assert.equal(parseFixedSlideHostSize(html), null);
123+
assert.ok(Date.now() - t0 < 1000, 'must finish quickly on large base64 blobs');
124+
});
125+
111126
it('ignores tiny decorative boxes', () => {
112127
const css = `.slide { width: 10px; height: 10px; }`;
113128
assert.equal(parseFixedSlideHostSize(css), null);

0 commit comments

Comments
 (0)