Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions site/plugins/remark-source-code.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ test('includes a named region inside a nested fence', (t) => {
assert.equal(quote.children[0].meta, null);
});

test('MDX ordered-list includes keep their list item and display title', (t) => {
const { root } = fixture(t);
const markdown = '1. Configure the file:\n\n ```go title="~/.netrc" file=server.go#handler\n ```';
const [list] = render(markdown, root, 'install.mdx').children;
assert.equal(list.type, 'list');
assert.equal(list.ordered, true);
const [item] = list.children;
assert.equal(item.type, 'listItem');
const code = item.children.find((node) => node.type === 'code');
assert.equal(code.value, 'func handler() {}');
assert.equal(code.meta, 'title="~/.netrc"');
const loader = withSourceCode({ name: 'test', load: (ctx) => ctx.generateDigest(markdown) }, { root });
const context = { generateDigest: JSON.stringify };
const before = loader.load(context);
writeFileSync(join(root, 'server.go'), '// docs:start handler\nfunc changed() {}\n// docs:end handler\n');
assert.notEqual(loader.load(context), before);
});

test('includes hash-comment regions from shell and workflow sources', (t) => {
const { root } = fixture(t);
writeFileSync(join(root, 'commands.sh'), '# docs:start install\n go mod verify\n# docs:end install\n');
Expand Down
18 changes: 11 additions & 7 deletions site/scripts/pro-publication.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const reviewedImport = "import ConfigReference from '../../../../components/Conf
const comments = /^\/\* Diataxis: (tutorial|how-to|reference|explanation)(; generated by sitedocs)? \*\/$/;
const declarationPackages = new Set(['admin', 'audit', 'audit/auditpg', 'audit/auditadmin', 'auth', 'auth/authbind', 'execution', 'execution/pgstore', 'execution/executionadmin', 'quota', 'quota/redisstore', 'quota/quotaadmin', 'quota/quotaotel', 'quota/quotaprom', 'quota/quotamcp', 'quota/quotastripe', 'quota/quotastripe/stripeadmin', 'tenant', 'webhook', 'webhook/webhookmem', 'webhook/webhookpg', 'webhook/webhookadmin']);
const markdownNodes = new Set(['root', 'yaml', 'paragraph', 'text', 'heading', 'thematicBreak', 'blockquote', 'list', 'listItem', 'emphasis', 'strong', 'delete', 'break', 'inlineCode', 'code', 'link', 'image', 'definition', 'linkReference', 'imageReference']);
// Match the complete line, exactly as the Go exporter's sourceFence does.
const sourceFence = /^ {0,3}```[a-zA-Z][a-zA-Z0-9_-]*(?: title="[a-zA-Z0-9_./~ -]+")? file=site\/pro-source\/([a-zA-Z0-9_.\/#-]+)$/;

export function checkDestination(name, input, files) {
let destination = input.replace(/&(#x[0-9a-f]+|#\d+|[a-z][a-z0-9]+);/gi, (whole, entity) => {
Expand Down Expand Up @@ -150,13 +152,15 @@ function checkPublicationSyntax(name, body, files) {
const end = trimmed.indexOf(']');
if (end < 0 || /[\[\\]/.test(trimmed.slice(1, end))) fail('unsupported reference label');
}
if (trimmed.startsWith('~~~') || trimmed.startsWith('```') && !line.startsWith('```')) fail('unsupported source fence');
if (line.startsWith('```')) {
if (line.slice(3).includes('`')) fail('backtick in fence info');
if (lines[++i] !== '```') fail('source fences must be empty');
const attrs = line.slice(3).trim().split(/\s+/);
if (attrs.length !== 2 || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(attrs[0]) || !attrs[1].startsWith('file=site/pro-source/')) fail('every code fence must be an empty source include');
const [path, region, extra] = attrs[1].slice('file=site/pro-source/'.length).split('#');
const fence = line.replace(/^ +/, '');
const indent = line.slice(0, line.length - fence.length);
if (trimmed.startsWith('~~~') || trimmed.startsWith('```') && (!fence.startsWith('```') || indent.length > 3)) fail('unsupported source fence');
if (trimmed.startsWith('```')) {
if (fence.slice(3).includes('`')) fail('backtick in fence info');
if (lines[++i] !== `${indent}\x60\x60\x60`) fail('source fences must be empty');
const match = sourceFence.exec(line);
if (!match) fail('every code fence must be an empty source include');
const [path, region, extra] = match[1].split('#');
if (!path.startsWith('examples/') || !files.has(path)) fail('unexported snippet');
if (region !== undefined) {
const escaped = region.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
Expand Down
31 changes: 29 additions & 2 deletions site/scripts/pro-publication.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,30 @@ const reviewedImport = "import ConfigReference from '../../../../components/Conf
const api = { configs: [{ name: 'auth.Config' }] };
const files = new Map();

test('source fences accept up to three spaces and one restricted literal title', () => {
const source = new Map([['examples/demo.sh', '# docs:start install\ngo mod verify\n# docs:end install\n']]);
const file = 'file=site/pro-source/examples/demo.sh#install';
const check = (content) => checkMDX('pages/install.mdx', content, source, api);
for (const indent of ['', ' ', ' ', ' ']) {
for (const attrs of [file, `title="~/.netrc" ${file}`, `title="A-Z_a-z/0.9 ~" ${file}`]) {
assert.doesNotThrow(() => check(`${indent}\x60\x60\x60sh ${attrs}\n${indent}\x60\x60\x60`));
}
}
assert.doesNotThrow(() => check(`1. Install:\n\n \x60\x60\x60sh title="~/.netrc" ${file}\n \x60\x60\x60`));
for (const indent of [' ', ' ', '\t', ' \t']) {
assert.throws(() => check(`${indent}\x60\x60\x60sh ${file}\n${indent}\x60\x60\x60`), /unsupported source fence/);
}
for (const close of ['```', ' ```', ' ```', ' ``` ', ' ````']) {
assert.throws(() => check(` \x60\x60\x60sh ${file}\n${close}`), /source fences must be empty/);
}
for (const attrs of [
`title="" ${file}`, `title='demo' ${file}`, `title={demo} ${file}`,
`title="a" title="b" ${file}`, `title="a" extra ${file}`,
`${file} title="A-Z_a-z/0.9 ~"`,
...['{x}', '<x>', 'a:b', 'a#b', 'a=b', 'a\\b', 'a\tb', 'café', 'a"b', 'a`b'].map((title) => `title="${title}" ${file}`),
]) assert.throws(() => check(`\x60\x60\x60sh ${attrs}\n\x60\x60\x60`), undefined, attrs);
});

test('only reviewed MDX imports, components and literal attributes are accepted', () => {
for (const content of [
"import fs from 'node:fs';", "> import fs from 'node:fs';", "- import fs from 'node:fs';", "import secret from '/private/source.go?raw';",
Expand Down Expand Up @@ -105,12 +129,15 @@ test('synthetic corpus traverses readPro and its source include validation', asy
for (const c of corpus.cases.filter((c) => !c.currentPage)) await t.test(c.id, (t) => {
const root = mkdtempSync(join(tmpdir(), 'pro-publication-corpus-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
// The shared grammar fixture uses a generic filename; materialize it at
// an already reviewed bundle path without widening the export allowlist.
const bundlePath = (value) => value.replaceAll('examples/demo.sh', 'examples/site-docs/commands.sh');
const payloads = {
...corpus.files,
...Object.fromEntries(Object.entries(corpus.files).map(([name, content]) => [bundlePath(name), content])),
'go.mod': 'module echo.labstack.com/pro-site-snapshot\n\ngo 1.26.0\n',
'config-fields.json': JSON.stringify({ ...api, module: 'echo.labstack.com/pro', revision: 'v0.1.0' }),
'problems.json': '[]',
[c.name]: c.content,
[c.name]: bundlePath(c.content),
};
const provenance = { module: 'echo.labstack.com/pro', version: 'v0.1.0', commit: 'a'.repeat(40), hashes: {} };
for (const [name, content] of Object.entries(payloads)) {
Expand Down
145 changes: 143 additions & 2 deletions site/scripts/testdata/publication-corpus.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"files": {
"pages/auth.mdx": "",
"examples/getting-started/main.go": "// docs:start demo\npackage main\n// docs:end demo\n"
"examples/getting-started/main.go": "// docs:start demo\npackage main\n// docs:end demo\n",
"examples/demo.sh": "echo demo\n"
},
"cases": [
{
Expand Down Expand Up @@ -299,12 +300,152 @@
"reason": "unsupported source fence"
},
{
"id": "nested-fence- ",
"id": "indented-source-fence",
"name": "pages/reference/auth.mdx",
"content": " ```go file=site/pro-source/examples/getting-started/main.go\n ```",
"allowed": true,
"reason": ""
},
{
"id": "unicode-space-in-path",
"name": "pages/reference/auth.mdx",
"content": "```sh file=site/pro-source/examples/demo.sh#a b\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-joined-language",
"name": "pages/reference/auth.mdx",
"content": "```g title=\"x\"o file=site/pro-source/examples/demo.sh\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-joined-path",
"name": "pages/reference/auth.mdx",
"content": "```sh file=site/pro-source/examples/de title=\"x\"mo.sh\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-title-before-language",
"name": "pages/reference/auth.mdx",
"content": "``` title=\"x\" sh file=site/pro-source/examples/demo.sh\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-two-titles",
"name": "pages/reference/auth.mdx",
"content": "```sh title=\"x\" title=\"y\" file=site/pro-source/examples/demo.sh\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-title-after-file",
"name": "pages/reference/auth.mdx",
"content": "```sh file=site/pro-source/examples/demo.sh title=\"x\"\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-tab-indent",
"name": "pages/reference/auth.mdx",
"content": "\t```sh file=site/pro-source/examples/demo.sh\n\t```",
"allowed": false,
"reason": "unsupported source fence"
},
{
"id": "source-fence-closing-trailing-space",
"name": "pages/reference/auth.mdx",
"content": "```sh file=site/pro-source/examples/demo.sh\n``` ",
"allowed": false,
"reason": "source fences must be empty"
},
{
"id": "source-fence-closing-four-backticks",
"name": "pages/reference/auth.mdx",
"content": "```sh file=site/pro-source/examples/demo.sh\n````",
"allowed": false,
"reason": "source fences must be empty"
},
{
"id": "source-fence-single-quoted-title",
"name": "pages/reference/auth.mdx",
"content": "```sh title='x' file=site/pro-source/examples/demo.sh\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-title-open-brace",
"name": "pages/reference/auth.mdx",
"content": "```sh title=\"x{y\" file=site/pro-source/examples/demo.sh\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-title-close-brace",
"name": "pages/reference/auth.mdx",
"content": "```sh title=\"x}y\" file=site/pro-source/examples/demo.sh\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-title-double-quote",
"name": "pages/reference/auth.mdx",
"content": "```sh title=\"x\"y\" file=site/pro-source/examples/demo.sh\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-title-backtick",
"name": "pages/reference/auth.mdx",
"content": "```sh title=\"x`y\" file=site/pro-source/examples/demo.sh\n```",
"allowed": false,
"reason": "backtick in fence info"
},
{
"id": "indented-source-fence-with-title",
"name": "pages/reference/auth.mdx",
"content": " ```sh title=\"~/.demo file-1_2\" file=site/pro-source/examples/demo.sh\n ```",
"allowed": true,
"reason": ""
},
{
"id": "source-fence-with-title",
"name": "pages/reference/auth.mdx",
"content": "```sh title=\"~/.demo file-1_2\" file=site/pro-source/examples/demo.sh\n```",
"allowed": true,
"reason": ""
},
{
"id": "source-fence-leading-space",
"name": "pages/reference/auth.mdx",
"content": "``` sh file=site/pro-source/examples/demo.sh\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-double-space",
"name": "pages/reference/auth.mdx",
"content": "```sh file=site/pro-source/examples/demo.sh\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-tab-separator",
"name": "pages/reference/auth.mdx",
"content": "```sh\tfile=site/pro-source/examples/demo.sh\n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence-trailing-space",
"name": "pages/reference/auth.mdx",
"content": "```sh file=site/pro-source/examples/demo.sh \n```",
"allowed": false,
"reason": "every code fence must be an empty source include"
},
{
"id": "source-fence",
"name": "pages/reference/auth.mdx",
Expand Down
Loading