diff --git a/CLAUDE.md b/CLAUDE.md index 1432605..e408596 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,7 +85,7 @@ Orchestrator: `scripts/build-vite.js`. Flags: `--local`, `--headless`. - `scripts/artifacts.js` — Safe tarball extraction + tree copy, shared by both fetch paths. Validates archive members (no traversal, no absolute paths, no symlinks) before anything is written, and replaces the old `cp -r`/`tar` shell-outs so the build runs on Windows - `scripts/hoist-inline-scripts.js` — Moves inline `', + body: + '

Section

' + + '

Write <base href="/"> or onclick="x()" in prose freely.

' + + 'Guide Top Intro' + + 'External PDF Mail' + + ' ', + }), + 'guide/index.html': page(), + 'assets/app.js': 'fetch(new URL("data.json", import.meta.url)); fetch("/api/x"); fetch(`${base}/y`);' + + 's.open("GET","function"==typeof e?e(n):e,!0);', + }), []); +}); + +console.log('\nManifest and artifact'); + +check('KB-ART-001: a missing entry point', () => { + expectIds(site('no-entry', { 'other.html': page() }), ['KB-ART-001']); +}); + +check('KB-ART-002: a pages entry that is not in the output', () => { + const app = { ...APP, pages: [{ title: 'Gone', path: 'gone/index.html', order: 1 }] }; + expectIds(site('gone-page', { 'index.html': page() }, app), ['KB-ART-002']); +}); + +check('KB-ART-003: no HTML at all', () => { + expectIds(site('no-html', { 'readme.txt': 'x' }, { ...APP, entryPoint: 'readme.txt' }), ['KB-ART-003']); +}); + +console.log('\nHTML'); + +check('KB-HTML-001: a page without the headless marker, and the legacy marker by name', () => { + const missing = site('no-marker', { 'index.html': page({ html: 'lang="en"' }) }); + expectIds(missing, ['KB-HTML-001']); + assert.match(missing[0].message, /missing data-kb-headless/); + const legacy = site('legacy-marker', { 'index.html': page({ html: 'data-mp-headless="true"' }) }); + assert.match(legacy[0].message, /data-mp-headless.*pre-v1/); +}); + +check('KB-HTML-002: a element', () => { + expectIds(site('base', { 'index.html': page({ head: '' }) }), ['KB-HTML-002']); +}); + +check('KB-HTML-003: root-relative URLs, named with an example', () => { + const f = site('root-relative', { 'index.html': page({ body: 'G' }) }); + expectIds(f, ['KB-HTML-003']); + assert.match(f[0].message, /2 root-relative URL\(s\), e\.g\. "\/images\/logo\.png"/); +}); + +check('KB-HTML-004: an executable inline script, not a data block', () => { + expectIds(site('inline', { 'index.html': page({ body: '' }) }), ['KB-HTML-004']); +}); + +check('KB-HTML-005: an inline event handler', () => { + const f = site('handler', { 'index.html': page({ body: '' }) }); + expectIds(f, ['KB-HTML-005']); + assert.match(f[0].message, / + + {{ page.content }} + + + + + diff --git a/tests/fixtures/kb-docs-add/onboarded-mkdocs/theme/style.css b/tests/fixtures/kb-docs-add/onboarded-mkdocs/theme/style.css new file mode 100644 index 0000000..83785b8 --- /dev/null +++ b/tests/fixtures/kb-docs-add/onboarded-mkdocs/theme/style.css @@ -0,0 +1,7 @@ +:root { --bg-page: #f8f9fb; --text-body: #374151; } +body { font-family: 'Source Sans 3', sans-serif; } +.topnav { position: fixed; top: 0; height: 56px; } +#sidebar { position: sticky; top: 0; } +#content h1 { color: #1b0e12 !important; } +#content a { text-decoration: underline !important; } +.dark body { background: #111827; color: #e5e7eb; } diff --git a/tests/fixtures/kb-docs-add/onboarded-mkdocs/theme/versions.json b/tests/fixtures/kb-docs-add/onboarded-mkdocs/theme/versions.json new file mode 100644 index 0000000..28054e7 --- /dev/null +++ b/tests/fixtures/kb-docs-add/onboarded-mkdocs/theme/versions.json @@ -0,0 +1,4 @@ +[ + { "title": "latest", "path": "" }, + { "title": "2.x", "path": "2.x/" } +] diff --git a/tests/transform.spec.js b/tests/transform.spec.js index fac5e81..eb266ba 100644 --- a/tests/transform.spec.js +++ b/tests/transform.spec.js @@ -12,7 +12,7 @@ */ import { test, expect } from '@playwright/test'; -import { transformSubAppHtml, isThemeBootstrap } from '../src/utils/transform.js'; +import { transformSubAppHtml, isThemeBootstrap, rewriteCssUrls } from '../src/utils/transform.js'; import { layerSubAppCss, LAYER_ORDER, SUB_APP_LAYER } from '../src/utils/css-layers.js'; const PREFIX = 'knowledge-base'; @@ -116,6 +116,24 @@ test.describe('URL rewriting', () => { expect(bodyHtml).toContain('url(/knowledge-base/demo/bg.png)'); expect(headHtml).toContain("url('/knowledge-base/demo/docs/img/hero.png')"); }); + + test('a stylesheet file resolves its URLs against its own path, never the page', () => { + const css = rewriteCssUrls( + '@import "base.css";\n@import url(../theme/t.css) screen;\n' + + ".a{background:url('img/x.png')}\n.b{background:url(/root.png)}\n" + + '.c{background:url(data:image/gif;base64,R0)}\n.d{mask:url(#m)}\n' + + '.e{background:url(https://cdn.example.com/y.png)}\n.f{background:url(//cdn.example.com/z.png)}', + '/knowledge-base/demo/assets/css/site.css', PREFIX, SLUG, + ); + expect(css).toContain('@import "/knowledge-base/demo/assets/css/base.css";'); + expect(css).toContain('@import url(/knowledge-base/demo/assets/theme/t.css) screen;'); + expect(css).toContain("url('/knowledge-base/demo/assets/css/img/x.png')"); + expect(css).toContain('url(/knowledge-base/demo/root.png)'); + expect(css).toContain('url(data:image/gif;base64,R0)'); + expect(css).toContain('url(#m)'); + expect(css).toContain('url(https://cdn.example.com/y.png)'); + expect(css).toContain('url(//cdn.example.com/z.png)'); + }); }); // Inside a web fragment a sub-app stylesheet can outlive its page (reframed