edgepress 2026.10.12 → 2026.10.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -2
- package/config.yml +171 -171
- package/content/assets/edgepress/brand/sources/export-manifest.json +181 -181
- package/content/assets/edgepress-markdown-guide.svg +21 -21
- package/content/guides/markdown-media.md +38 -0
- package/content/guides/product-demos.md +30 -30
- package/content/pages/deployment/en.json +1 -1
- package/content/posts/2026-09-24-edgepress-release-2026-9-1/en.md +1 -1
- package/content/posts/2026-09-24-edgepress-release-2026-9-1/zh-cn.md +1 -1
- package/languages/base/en.json +98 -98
- package/languages/packs/zh-CN.json +98 -98
- package/package.json +90 -90
- package/src/assets.js +187 -186
- package/src/browser-audit.js +158 -158
- package/src/build.js +293 -291
- package/src/cli.js +364 -364
- package/src/content.js +233 -233
- package/src/data-saver.js +3 -3
- package/src/generators.js +414 -420
- package/src/i18n.js +86 -86
- package/src/markdown.js +195 -160
- package/src/page-check.js +1 -1
- package/src/plugin-api.js +77 -77
- package/src/report-visuals.js +642 -642
- package/src/revision-cards.js +30 -30
- package/src/rss.js +100 -0
- package/src/theme.js +4 -4
- package/src/video-metadata.js +36 -0
- package/static/edgepress/fonts/faces.css +2217 -2217
- package/static/edgepress/media-viewer.js +67 -10
- package/static/edgepress/project-demo.js +83 -83
- package/static/edgepress/search.js +84 -84
- package/tests/assets-runtime.test.mjs +26 -26
- package/tests/audit.test.mjs +142 -142
- package/tests/brand-regression.test.mjs +110 -110
- package/tests/document-package.test.mjs +2 -2
- package/tests/embed-syntax.test.mjs +68 -68
- package/tests/image-viewer.test.mjs +36 -36
- package/tests/markdown-media.test.mjs +119 -0
- package/tests/oembed-layout.test.mjs +6 -1
- package/tests/project-demo.test.mjs +73 -73
- package/tests/release-version.test.mjs +28 -0
- package/tests/rss.test.mjs +54 -0
- package/tests/video-metadata.test.mjs +28 -0
- package/tests/writing.test.mjs +83 -83
- package/themes/atelier/assets/style.css +340 -340
- package/themes/default/assets/style.css +371 -371
- package/themes/default/layouts/partials/head.html +1 -1
- package/themes/folio/assets/style.css +515 -515
- package/themes/folio/theme.json +5 -5
- package/themes/signal/assets/style.css +332 -332
- package/tools/README.md +10 -10
- package/tools/assets/generate-brand-assets.py +160 -160
- package/tools/assets/video-preview.mjs +15 -0
- package/tools/recordings/demo-workbench.mjs +53 -53
- package/tools/recordings/desktop-recording.mjs +70 -70
- package/tools/recordings/vm-desktop.mjs +59 -59
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { generateRssRoute, absoluteContentUrls } from '../src/rss.js';
|
|
4
|
+
|
|
5
|
+
const config = {
|
|
6
|
+
site: { url: 'https://example.test', title: 'A & B', description: 'News <analysis>', author: { name: 'Editor' } },
|
|
7
|
+
i18n: { defaultLocale: 'en', locales: ['en', 'zh-CN'] },
|
|
8
|
+
markdown: { gfm: true, breaks: false }
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function post(overrides = {}) {
|
|
12
|
+
return {
|
|
13
|
+
title: 'Title & <tag>', author: 'Writer', locale: 'en', path: '2026/10/scene/',
|
|
14
|
+
date: new Date('2026-10-15T09:00:00Z'), updated: new Date('2026-10-17T10:00:00Z'),
|
|
15
|
+
category: 'science', tags: ['AI', 'science'],
|
|
16
|
+
description: 'Summary & details',
|
|
17
|
+
markdown: '## Introduction\n\nLong **article** text with [local link](/notes/?a=1&b=2), a  and <script>unsafe()</script>.\n\nA second paragraph.',
|
|
18
|
+
...overrides
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('RSS 2.0 gives AI readers a stable canonical item, full safe HTML, and correct update metadata', async () => {
|
|
23
|
+
const route = await generateRssRoute([post()], config, 'en');
|
|
24
|
+
assert.equal(route.path, 'rss.xml');
|
|
25
|
+
assert.equal(route.contentType, 'application/rss+xml; charset=utf-8');
|
|
26
|
+
assert.match(route.body, /<rss version="2\.0" /);
|
|
27
|
+
assert.match(route.body, /<atom:link href="https:\/\/example\.test\/rss\.xml" rel="self" type="application\/rss\+xml"\/>/);
|
|
28
|
+
assert.match(route.body, /<title>Title & <tag><\/title>/);
|
|
29
|
+
assert.match(route.body, /<guid isPermaLink="true">https:\/\/example\.test\/2026\/10\/scene\/<\/guid>/);
|
|
30
|
+
assert.match(route.body, /<dc:creator>Writer<\/dc:creator>/);
|
|
31
|
+
assert.match(route.body, /<dc:date>2026-10-17T10:00:00\.000Z<\/dc:date>/);
|
|
32
|
+
assert.match(route.body, /<pubDate>Thu, 15 Oct 2026 09:00:00 GMT<\/pubDate>/);
|
|
33
|
+
assert.match(route.body, /<lastBuildDate>Sat, 17 Oct 2026 10:00:00 GMT<\/lastBuildDate>/);
|
|
34
|
+
assert.match(route.body, /<content:encoded>[\s\S]*Introduction[\s\S]*second paragraph[\s\S]*<\/content:encoded>/i);
|
|
35
|
+
assert.match(route.body, /https:\/\/example\.test\/images\/scene\.webp/);
|
|
36
|
+
assert.match(route.body, /https:\/\/example\.test\/notes\/\?a=1&amp;b=2/);
|
|
37
|
+
assert.doesNotMatch(route.body, /unsafe\(\)|<script|<iframe/);
|
|
38
|
+
assert.equal((route.body.match(/<category>/g) || []).length, 2);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('RSS locale paths, item limit, empty feeds and special characters', async () => {
|
|
42
|
+
const localized = await generateRssRoute([post({ title: '中文 & 新闻', locale: 'zh-CN', path: 'zh-CN/中文/' })], config, 'zh-CN');
|
|
43
|
+
assert.equal(localized.path, 'zh-CN/rss.xml');
|
|
44
|
+
assert.match(localized.body, /<language>zh-CN<\/language>/);
|
|
45
|
+
assert.match(localized.body, /<dc:language>zh-CN<\/dc:language>/);
|
|
46
|
+
assert.match(localized.body, /%E4%B8%AD%E6%96%87/);
|
|
47
|
+
assert.match(localized.body, /中文 & 新闻/);
|
|
48
|
+
const empty = await generateRssRoute([], config, 'en');
|
|
49
|
+
assert.doesNotMatch(empty.body, /<item>/);
|
|
50
|
+
const limited = await generateRssRoute(Array.from({ length: 22 }, (_, i) => post({ title: String(i) })), config, 'en');
|
|
51
|
+
assert.equal((limited.body.match(/<item>/g) || []).length, 20);
|
|
52
|
+
assert.match(absoluteContentUrls('<a href="../x?a=1&b=2">X</a>', 'https://example.test/section/article/'),
|
|
53
|
+
/href="https:\/\/example\.test\/section\/x\?a=1&b=2"/);
|
|
54
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {collectVideoMetadata, reserveVideoMetadata} from '../src/video-metadata.js';
|
|
4
|
+
import {dataSaverHtml, configureDataSaver} from '../src/data-saver.js';
|
|
5
|
+
import {loadConfig} from '../src/config.js';
|
|
6
|
+
|
|
7
|
+
test('local video sidecars reserve measured previews without downloading media and survive text-view gating', async () => {
|
|
8
|
+
const assets = [
|
|
9
|
+
{path:'media/clip.mp4'}, {path:'media/clip.mp4.poster.jpg'},
|
|
10
|
+
{path:'media/clip.mp4.json', content:Buffer.from(JSON.stringify({duration:150.001,width:960,height:540,poster:'clip.mp4.poster.jpg'}))}
|
|
11
|
+
];
|
|
12
|
+
const metadata = await collectVideoMetadata(assets);
|
|
13
|
+
const source = '<figure><video data-edgepress-video preload="none" aria-label="Clip"><source src="/media/clip.mp4" type="video/mp4"></video><figcaption>Caption</figcaption></figure>';
|
|
14
|
+
const html = reserveVideoMetadata(source, metadata, '2026/10/report/index.html', 'https://site.example');
|
|
15
|
+
assert.match(html, /data-duration="150\.001" poster="\/media\/clip\.mp4\.poster\.jpg" width="960" height="540"/);
|
|
16
|
+
assert.equal(reserveVideoMetadata(html, metadata, 'index.html', 'https://site.example'),html);
|
|
17
|
+
assert.equal(reserveVideoMetadata(source.replace('/media/clip.mp4','https://other.example/media/clip.mp4'),metadata,'index.html','https://site.example'),source.replace('/media/clip.mp4','https://other.example/media/clip.mp4'));
|
|
18
|
+
assert.match(reserveVideoMetadata(source.replace('preload="none"','preload="none" poster="/chosen.jpg"'),metadata,'index.html','https://site.example'),/poster="\/chosen.jpg"/);
|
|
19
|
+
const config = await loadConfig(process.cwd()); config.site.dataSaver.enabled=true; await configureDataSaver(config);
|
|
20
|
+
const gated = dataSaverHtml('<html><head></head><body>'+html+'</body></html>',config,{});
|
|
21
|
+
assert.match(gated,/<template data-edgepress-data-html>.*data-duration="150\.001".*poster="\/media\/clip\.mp4\.poster\.jpg"/s);
|
|
22
|
+
assert.match(gated,/<\/div><figcaption>Caption<\/figcaption>/);
|
|
23
|
+
assert.deepEqual(await collectVideoMetadata([{path:'media/clip.mp4'}]),new Map());
|
|
24
|
+
for (const change of [{duration:0},{poster:'https://external.example/image.jpg'},{poster:'missing.jpg'}]) {
|
|
25
|
+
const bad=assets.map(item=>item.path.endsWith('.json')?{...item,content:Buffer.from(JSON.stringify({duration:150,width:960,height:540,poster:'clip.mp4.poster.jpg',...change}))}:item);
|
|
26
|
+
await assert.rejects(collectVideoMetadata(bad));
|
|
27
|
+
}
|
|
28
|
+
});
|
package/tests/writing.test.mjs
CHANGED
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { resolve } from 'node:path';
|
|
6
|
-
import { spawnSync } from 'node:child_process';
|
|
7
|
-
import { postTemplate } from '../src/post-template.js';
|
|
8
|
-
import { readDocuments } from '../src/content.js';
|
|
9
|
-
import { renderMarkdown } from '../src/markdown.js';
|
|
10
|
-
import { renderIcon } from '../src/icons.js';
|
|
11
|
-
import { parse, stringify } from 'yaml';
|
|
12
|
-
|
|
13
|
-
test('new post metadata round-trips quotes and Markdown titles, with one page title', async () => {
|
|
14
|
-
const root = await mkdtemp(resolve(tmpdir(), 'edgepress-writing-'));
|
|
15
|
-
try {
|
|
16
|
-
const content = resolve(root, 'content');
|
|
17
|
-
await mkdir(resolve(content, 'posts/2026-10-02-writing'), { recursive: true });
|
|
18
|
-
const title = '中文 "title": **Markdown** <script>';
|
|
19
|
-
const source = postTemplate(title, '2026-10-02', 'zh-CN', '在这里写正文。');
|
|
20
|
-
await writeFile(resolve(content, 'posts/2026-10-02-writing/zh-cn.md'), source);
|
|
21
|
-
const config = { resolvedPaths: { content }, i18n: { defaultLocale: 'zh-CN', locales: ['zh-CN'] }, concurrency: 1, permalink: '/:year/:month/:slug/' };
|
|
22
|
-
const [post] = await readDocuments(config, 'posts');
|
|
23
|
-
assert.equal(post.title, title);
|
|
24
|
-
assert.equal(post.locale, 'zh-CN');
|
|
25
|
-
assert.equal(post.date.toISOString().slice(0, 10), '2026-10-02');
|
|
26
|
-
assert.equal(post.markdown, '在这里写正文。');
|
|
27
|
-
assert.equal(post.category, 'uncategorized');
|
|
28
|
-
assert.doesNotMatch(await renderMarkdown(post.markdown), /<h1/);
|
|
29
|
-
} finally { await rm(root, { recursive: true, force: true }); }
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('article authors accept a profile object and unsafe avatar URLs fail at content loading', async () => {
|
|
33
|
-
const root = await mkdtemp(resolve(tmpdir(), 'edgepress-profile-'));
|
|
34
|
-
try {
|
|
35
|
-
const content = resolve(root, 'content');
|
|
36
|
-
const file = resolve(content, 'posts/profile/en.md');
|
|
37
|
-
await mkdir(resolve(content, 'posts/profile'), { recursive: true });
|
|
38
|
-
const config = { resolvedPaths: { content }, i18n: { locales: ['en'], defaultLocale: 'en' }, concurrency: 1, permalink: '/:slug/' };
|
|
39
|
-
const metadata = { title: 'Profile', date: '2026-10-02', author: { name: 'Guest', avatar: '/images/guest.png' }, category: '博客', tags: ['notes'] };
|
|
40
|
-
await writeFile(file, '---\n' + stringify(metadata) + '---\nText');
|
|
41
|
-
const [post] = await readDocuments(config);
|
|
42
|
-
assert.equal(post.author, 'Guest');
|
|
43
|
-
assert.equal(post.authorAvatar, '/images/guest.png');
|
|
44
|
-
assert.equal(post.category, 'blog');
|
|
45
|
-
metadata.author.avatar = 'javascript:alert(1)';
|
|
46
|
-
await writeFile(file, '---\n' + stringify(metadata) + '---\nText');
|
|
47
|
-
await assert.rejects(readDocuments(config), /author avatar/);
|
|
48
|
-
} finally { await rm(root, { recursive: true, force: true }); }
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('CLI creates a localized post and refuses to overwrite it', async () => {
|
|
52
|
-
const root = await mkdtemp(resolve(tmpdir(), 'edgepress-cli-writing-'));
|
|
53
|
-
try {
|
|
54
|
-
await writeFile(resolve(root, 'edgepress.config.mjs'), 'export default {i18n:{defaultLocale:"zh-CN",languagePacks:[]}};');
|
|
55
|
-
const settings = parse(await readFile(resolve('config.yml'), 'utf8'));
|
|
56
|
-
settings.plugins.consent.services = [];
|
|
57
|
-
await writeFile(resolve(root, 'config.yml'), stringify(settings));
|
|
58
|
-
await mkdir(resolve(root, 'languages/base'), { recursive: true });
|
|
59
|
-
await writeFile(resolve(root, 'languages/base/zh-CN.json'), JSON.stringify({ postStarter: '在这里写正文。', postCreated: '已创建 {file}', postNextSteps: '用 Markdown 写正文。', postPreview: 'npm run dev' }));
|
|
60
|
-
const cli = resolve('src/cli.js');
|
|
61
|
-
const first = spawnSync(process.execPath, [cli, 'new', 'post', '我的第一篇文章'], { cwd: root, encoding: 'utf8' });
|
|
62
|
-
assert.equal(first.status, 0, first.stderr);
|
|
63
|
-
assert.match(first.stdout, /已创建/);
|
|
64
|
-
const match = first.stdout.match(/content[^\r\n]+\.md/);
|
|
65
|
-
const file = resolve(root, match[0]);
|
|
66
|
-
const before = await readFile(file, 'utf8');
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { postTemplate } from '../src/post-template.js';
|
|
8
|
+
import { readDocuments } from '../src/content.js';
|
|
9
|
+
import { renderMarkdown } from '../src/markdown.js';
|
|
10
|
+
import { renderIcon } from '../src/icons.js';
|
|
11
|
+
import { parse, stringify } from 'yaml';
|
|
12
|
+
|
|
13
|
+
test('new post metadata round-trips quotes and Markdown titles, with one page title', async () => {
|
|
14
|
+
const root = await mkdtemp(resolve(tmpdir(), 'edgepress-writing-'));
|
|
15
|
+
try {
|
|
16
|
+
const content = resolve(root, 'content');
|
|
17
|
+
await mkdir(resolve(content, 'posts/2026-10-02-writing'), { recursive: true });
|
|
18
|
+
const title = '中文 "title": **Markdown** <script>';
|
|
19
|
+
const source = postTemplate(title, '2026-10-02', 'zh-CN', '在这里写正文。');
|
|
20
|
+
await writeFile(resolve(content, 'posts/2026-10-02-writing/zh-cn.md'), source);
|
|
21
|
+
const config = { resolvedPaths: { content }, i18n: { defaultLocale: 'zh-CN', locales: ['zh-CN'] }, concurrency: 1, permalink: '/:year/:month/:slug/' };
|
|
22
|
+
const [post] = await readDocuments(config, 'posts');
|
|
23
|
+
assert.equal(post.title, title);
|
|
24
|
+
assert.equal(post.locale, 'zh-CN');
|
|
25
|
+
assert.equal(post.date.toISOString().slice(0, 10), '2026-10-02');
|
|
26
|
+
assert.equal(post.markdown, '在这里写正文。');
|
|
27
|
+
assert.equal(post.category, 'uncategorized');
|
|
28
|
+
assert.doesNotMatch(await renderMarkdown(post.markdown), /<h1/);
|
|
29
|
+
} finally { await rm(root, { recursive: true, force: true }); }
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('article authors accept a profile object and unsafe avatar URLs fail at content loading', async () => {
|
|
33
|
+
const root = await mkdtemp(resolve(tmpdir(), 'edgepress-profile-'));
|
|
34
|
+
try {
|
|
35
|
+
const content = resolve(root, 'content');
|
|
36
|
+
const file = resolve(content, 'posts/profile/en.md');
|
|
37
|
+
await mkdir(resolve(content, 'posts/profile'), { recursive: true });
|
|
38
|
+
const config = { resolvedPaths: { content }, i18n: { locales: ['en'], defaultLocale: 'en' }, concurrency: 1, permalink: '/:slug/' };
|
|
39
|
+
const metadata = { title: 'Profile', date: '2026-10-02', author: { name: 'Guest', avatar: '/images/guest.png' }, category: '博客', tags: ['notes'] };
|
|
40
|
+
await writeFile(file, '---\n' + stringify(metadata) + '---\nText');
|
|
41
|
+
const [post] = await readDocuments(config);
|
|
42
|
+
assert.equal(post.author, 'Guest');
|
|
43
|
+
assert.equal(post.authorAvatar, '/images/guest.png');
|
|
44
|
+
assert.equal(post.category, 'blog');
|
|
45
|
+
metadata.author.avatar = 'javascript:alert(1)';
|
|
46
|
+
await writeFile(file, '---\n' + stringify(metadata) + '---\nText');
|
|
47
|
+
await assert.rejects(readDocuments(config), /author avatar/);
|
|
48
|
+
} finally { await rm(root, { recursive: true, force: true }); }
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('CLI creates a localized post and refuses to overwrite it', async () => {
|
|
52
|
+
const root = await mkdtemp(resolve(tmpdir(), 'edgepress-cli-writing-'));
|
|
53
|
+
try {
|
|
54
|
+
await writeFile(resolve(root, 'edgepress.config.mjs'), 'export default {i18n:{defaultLocale:"zh-CN",languagePacks:[]}};');
|
|
55
|
+
const settings = parse(await readFile(resolve('config.yml'), 'utf8'));
|
|
56
|
+
settings.plugins.consent.services = [];
|
|
57
|
+
await writeFile(resolve(root, 'config.yml'), stringify(settings));
|
|
58
|
+
await mkdir(resolve(root, 'languages/base'), { recursive: true });
|
|
59
|
+
await writeFile(resolve(root, 'languages/base/zh-CN.json'), JSON.stringify({ postStarter: '在这里写正文。', postCreated: '已创建 {file}', postNextSteps: '用 Markdown 写正文。', postPreview: 'npm run dev' }));
|
|
60
|
+
const cli = resolve('src/cli.js');
|
|
61
|
+
const first = spawnSync(process.execPath, [cli, 'new', 'post', '我的第一篇文章'], { cwd: root, encoding: 'utf8' });
|
|
62
|
+
assert.equal(first.status, 0, first.stderr);
|
|
63
|
+
assert.match(first.stdout, /已创建/);
|
|
64
|
+
const match = first.stdout.match(/content[^\r\n]+\.md/);
|
|
65
|
+
const file = resolve(root, match[0]);
|
|
66
|
+
const before = await readFile(file, 'utf8');
|
|
67
67
|
assert.match(before, /在这里写正文。/);
|
|
68
|
-
assert.equal(parse(before.split('---')[1]).title, '我的第一篇文章');
|
|
69
|
-
assert.doesNotMatch(before, /# 我的第一篇文章/);
|
|
70
|
-
const second = spawnSync(process.execPath, [cli, 'new', 'post', '我的第一篇文章'], { cwd: root, encoding: 'utf8' });
|
|
71
|
-
assert.equal(second.status, 1);
|
|
72
|
-
assert.match(second.stderr, /already exists/);
|
|
73
|
-
assert.equal(await readFile(file, 'utf8'), before);
|
|
74
|
-
} finally { await rm(root, { recursive: true, force: true }); }
|
|
75
|
-
});
|
|
76
|
-
|
|
68
|
+
assert.equal(parse(before.split('---')[1]).title, '我的第一篇文章');
|
|
69
|
+
assert.doesNotMatch(before, /# 我的第一篇文章/);
|
|
70
|
+
const second = spawnSync(process.execPath, [cli, 'new', 'post', '我的第一篇文章'], { cwd: root, encoding: 'utf8' });
|
|
71
|
+
assert.equal(second.status, 1);
|
|
72
|
+
assert.match(second.stderr, /already exists/);
|
|
73
|
+
assert.equal(await readFile(file, 'utf8'), before);
|
|
74
|
+
} finally { await rm(root, { recursive: true, force: true }); }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
77
|
test('CLI initialization carries root security overrides without discarding operator choices', async () => {
|
|
78
78
|
const root = await mkdtemp(resolve(tmpdir(), 'edgepress-cli-init-'));
|
|
79
79
|
try {
|
|
@@ -87,15 +87,15 @@ test('CLI initialization carries root security overrides without discarding oper
|
|
|
87
87
|
assert.equal(manifest.dependencies.wrangler, framework.devDependencies.wrangler);
|
|
88
88
|
assert.equal(manifest.scripts.verify, 'node verify.mjs');
|
|
89
89
|
assert.equal(manifest.name, 'operator-site');
|
|
90
|
-
const home = JSON.parse(await readFile(resolve(root, 'content/pages/home/en.json'), 'utf8'));
|
|
91
|
-
assert.equal(home.$schema, '../../../node_modules/edgepress/schemas/pages.schema.json');
|
|
92
|
-
assert.ok(home.blocks.length);
|
|
90
|
+
const home = JSON.parse(await readFile(resolve(root, 'content/pages/home/en.json'), 'utf8'));
|
|
91
|
+
assert.equal(home.$schema, '../../../node_modules/edgepress/schemas/pages.schema.json');
|
|
92
|
+
assert.ok(home.blocks.length);
|
|
93
93
|
} finally { await rm(root, { recursive: true, force: true }); }
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
test('bitmap icon paths reject traversal and preserve safe caller classes', () => {
|
|
97
|
-
assert.match(renderIcon('book-open', 'icon nav_icon'), /class="icon nav_icon icon-library"/);
|
|
98
|
-
assert.match(renderIcon('book-open'), /alt="" aria-hidden="true"/);
|
|
99
|
-
for (const name of ['../home', 'home.png', '__proto__', 'HOME', '" onerror="x']) assert.throws(() => renderIcon(name));
|
|
100
|
-
for (const value of ['icon" onerror="x', 'icon<svg', '', undefined + '"']) assert.throws(() => renderIcon('home', value));
|
|
101
|
-
});
|
|
97
|
+
assert.match(renderIcon('book-open', 'icon nav_icon'), /class="icon nav_icon icon-library"/);
|
|
98
|
+
assert.match(renderIcon('book-open'), /alt="" aria-hidden="true"/);
|
|
99
|
+
for (const name of ['../home', 'home.png', '__proto__', 'HOME', '" onerror="x']) assert.throws(() => renderIcon(name));
|
|
100
|
+
for (const value of ['icon" onerror="x', 'icon<svg', '', undefined + '"']) assert.throws(() => renderIcon('home', value));
|
|
101
|
+
});
|