mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
7f65c795d9
* refactor: share one structural HTML injection-point locator #7410's root cause was two copies of the same lookup drifting apart: the srcDoc path anchored on the real `</body>` while the daemon copy still matched the first one in the text. This puts the locator in `packages/contracts` and points every injector at it — daemon preview bridges and containment base, web srcDoc and export paths, desktop PDF/artifact/deck capture, and the brand build. Rebuilt on `main` rather than rebased onto it. This branch was cut before #7421 and carried its 28 commits, so a rebase replayed them against a main that already had them and conflicted on the first one; replaying 17 commits through that would have been the easy way to silently drop one of the fixes. The net change is the same 26 files, and only four of them had also moved on main. The shared module is the daemon implementation as merged, not a reconstruction. Verified as equivalence before the daemon's copy was deleted: against `main`'s `apps/daemon/src/routes/project/index.ts` on identical inputs, 2,544 repository HTML files × 7 lookups and 15,000 generated adversarial documents × 6 lookups produce byte-identical answers — 0 divergences. The generated corpus is built from every hazard #7421's review rounds turned up, which is the comparison that would have caught the drift this PR exists to prevent. Three call sites finished their migration on the way: `daemonSanitizeTitleInDoc`, `assembleExample` and the collab-sync title read all located `<title>` structurally and then searched for its close as text, so `</title >` and `</title\n>` were missed and a `</title` inside the open tag's own attributes could match first. All three go through `findRealElementRange` now. `findRealElementRange` itself built its shadow with `toLowerCase()`, which is not length-preserving; it uses `asciiLower` like the rest of the module. `pnpm guard` gains an "HTML structural boundaries" check that fails any new hand-rolled boundary lookup outside the shared module — the safeguard #7421's rationale comment once claimed and did not have. Validation: 100 daemon route/containment specs (#7421's full set, now running against the shared locator), 45 contracts specs, 817 web runtime specs including the jsdom differential oracle, guard exit 0 with the new check reporting, daemon and contracts typecheck clean. * fix: finish the title migration in srcdoc and the prefetch reader Two from review, both the same leftover: the open tag was located structurally and then the close was searched for as text. I had claimed this migration finished in three call sites and it was five — a sweep of the repository for `indexOf('</…')` found exactly these two remaining, and nothing else (the other hits are `<artifact>` / `<PANELIST>` marker streams, which are not HTML). `sanitizeTitleInDoc` used `indexOf('</title>')`, which accepts one spelling. A document titled with `</title >` fell through to the next `</title>` in the source — here one inside an authored script string — and the rewrite spanned everything between, splicing into author content exactly as #7410 did. `readRealTitleText` used `indexOf('</title')` with no boundary check, so `</title-page>` ended the title. Both go through `findRealElementRange` now, and with it the `toLowerCase()` shadow both were building, which is not length-preserving. Two specs. The `</title >` one is red on the previous commit. The `</title-page>` one is green on both — `indexOf('</title>')` happened to skip it too, since it demands the full spelling — so it is regression cover rather than a red spec, and it is worth having because the new code reaches that case through a different rule. Writing them cost three wrong assertions before one held, all mine rather than the code's: the title text is rewritten by `sanitizePreviewTitle`, so the raw authored bytes are not observable in the output, and `b</title>` — my second attempt at something that would be — is a substring of the sanitized `a-title-page-b</title>`. The spec asserts the sanitized title itself now. Validation: 819 web runtime specs, 100 daemon route/containment specs, `pnpm guard` exit 0 with the structural-boundary check reporting, web and daemon typecheck clean.
103 lines
4.2 KiB
TypeScript
103 lines
4.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { buildSrcdoc } from '../../src/runtime/srcdoc';
|
|
|
|
// These specs deliberately run with no `DOMParser` on globalThis (the web
|
|
// suite's default environment is node, and only the specs that need one stub
|
|
// it). That matters: `annotateMissingOdIds` early-returns without a parser, so
|
|
// nothing upstream normalizes the document into `<html><head>…`. What is left
|
|
// is each injector locating its own boundary — which is the property under
|
|
// test. Relying on an upstream round-trip to synthesize a `<head>` is exactly
|
|
// how the daemon copies of this logic stayed broken until #7410.
|
|
describe('buildSrcdoc injection points', () => {
|
|
it('leaves a script that builds an HTML document string intact', () => {
|
|
const authored = 'const doc = `<head><title>Slip</title></head><body>slip</body>`;';
|
|
const html = `<!doctype html><html><body><script>${authored}<\/script><p>hi</p></body></html>`;
|
|
|
|
const srcdoc = buildSrcdoc(html);
|
|
|
|
expect(srcdoc).toContain(authored);
|
|
});
|
|
|
|
it('leaves markup stored on an attribute intact', () => {
|
|
const authored = 'data-tpl="<head></head><body>slip</body>"';
|
|
const html = `<!doctype html><html><body><div ${authored}></div><p>hi</p></body></html>`;
|
|
|
|
const srcdoc = buildSrcdoc(html);
|
|
|
|
expect(srcdoc).toContain(authored);
|
|
});
|
|
|
|
it('does not mistake `<header>` for the document head', () => {
|
|
const html = '<!doctype html><html><body><header>nav</header><p>hi</p></body></html>';
|
|
|
|
const srcdoc = buildSrcdoc(html);
|
|
|
|
expect(srcdoc).toContain('<header>nav</header>');
|
|
});
|
|
|
|
it('keeps a deck bridge out of a script that writes `</body>`', () => {
|
|
const authored = 'const doc = `<body>slip</body>`;';
|
|
const html = `<!doctype html><html><head></head><body><script>${authored}<\/script><p>hi</p></body></html>`;
|
|
|
|
const srcdoc = buildSrcdoc(html, { deck: true });
|
|
|
|
expect(srcdoc).toContain(authored);
|
|
});
|
|
|
|
it('sanitizes a title whose open tag carries a quoted `>`', () => {
|
|
// The open tag ends at the real `>`, not the one inside the attribute
|
|
// value; ending early would swallow part of the title text.
|
|
const html = '<!doctype html><html><head><title data-x="a>b">Invoice Q1</title></head>'
|
|
+ '<body><p>hi</p></body></html>';
|
|
|
|
const srcdoc = buildSrcdoc(html);
|
|
|
|
expect(srcdoc).toContain('<title data-x="a>b">Invoice Q1</title>');
|
|
});
|
|
|
|
it('ends a title at a close the parser accepts, not the one spelling', () => {
|
|
// `</title >` closes the element. A `indexOf('</title>')` misses it and
|
|
// takes the next `</title>` in the document — here one inside an authored
|
|
// script string — so the rewrite range spans everything between.
|
|
const html = '<!doctype html><html><head><title>Invoice Q1</title ></head>'
|
|
+ '<body><script>const doc = `<title>Slip</title>`;<\/script>'
|
|
+ '<p>hi</p></body></html>';
|
|
|
|
const srcdoc = buildSrcdoc(html);
|
|
|
|
expect(srcdoc).toContain('<title>Invoice Q1</title >');
|
|
expect(srcdoc).toContain('const doc = `<title>Slip</title>`;');
|
|
});
|
|
|
|
it('does not accept a longer tag name as the title close', () => {
|
|
// `</title-page>` is not a close, so the whole `a</title-page>b` is title
|
|
// text and is sanitized as one string. Taking it as the close would title
|
|
// the document `a` and leave `b` as stray text before `</head>`.
|
|
const html = '<!doctype html><html><head><title>a</title-page>b</title></head>'
|
|
+ '<body><p>hi</p></body></html>';
|
|
|
|
const srcdoc = buildSrcdoc(html);
|
|
|
|
expect(srcdoc).toContain('<title>a-title-page-b</title>');
|
|
});
|
|
|
|
it('keeps every injected bridge outside the authored script', () => {
|
|
const authored = 'const doc = `<head></head><body>slip</body>`;';
|
|
const html = `<!doctype html><html><body><script>${authored}<\/script><p>hi</p></body></html>`;
|
|
|
|
const srcdoc = buildSrcdoc(html, {
|
|
deck: true,
|
|
editBridge: true,
|
|
selectionBridge: true,
|
|
previewFocusGuard: true,
|
|
previewObservability: true,
|
|
});
|
|
|
|
const scriptStart = srcdoc.indexOf('const doc = `');
|
|
const scriptEnd = srcdoc.indexOf('<\/script>', scriptStart);
|
|
const authoredScript = srcdoc.slice(scriptStart, scriptEnd);
|
|
expect(authoredScript).not.toMatch(/data-od-/);
|
|
});
|
|
});
|