mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Publish what each call writes for chosen corpus examples, and fail the gate where the page is stale
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import * as library from '@larvit/adf-codec'
|
||||
|
||||
import { renderFeatureTable } from './render.mjs'
|
||||
|
||||
const pagePath = new URL('../docs/feature-table.md', import.meta.url)
|
||||
const readJson = (url) => JSON.parse(readFileSync(url, 'utf8'))
|
||||
|
||||
const examples = readJson(new URL('examples.json', import.meta.url)).map(({ id, name }) => ({ doc: readJson(new URL(`../corpus/round-trip/${id}.json`, import.meta.url)), name }))
|
||||
const page = renderFeatureTable(examples, library)
|
||||
|
||||
if (!process.argv.includes('--check')) writeFileSync(pagePath, page)
|
||||
else if (readFileSync(pagePath, 'utf8') !== page) {
|
||||
console.error('docs/feature-table.md differs from what the examples and the build give: run ./feature-table/write.sh')
|
||||
process.exitCode = 1
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{ "id": "combinations/bold-command", "name": "Bold instruction with a command in it" },
|
||||
{ "id": "commonmark-subset/em-strong-strike", "name": "Emphasis and strikethrough" },
|
||||
{ "id": "commonmark-subset/heading", "name": "Headings" },
|
||||
{ "id": "commonmark-subset/bullet-list", "name": "Bullet list" },
|
||||
{ "id": "commonmark-subset/code-block", "name": "Code block" },
|
||||
{ "id": "commonmark-subset/blockquote", "name": "Quote" },
|
||||
{ "id": "commonmark-subset/link", "name": "Link" },
|
||||
{ "id": "block-nodes/table-plain", "name": "Table" },
|
||||
{ "id": "block-nodes/panel", "name": "Panels" },
|
||||
{ "id": "block-nodes/task-list", "name": "Task list" },
|
||||
{ "id": "block-nodes/image", "name": "Image" },
|
||||
{ "id": "inline-nodes/mention", "name": "Mention" },
|
||||
{ "id": "inline-nodes/status", "name": "Status" },
|
||||
{ "id": "inline-nodes/date", "name": "Date" },
|
||||
{ "id": "opaque-carry/unknown-block", "name": "Node types this version does not know" }
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
|
||||
const writers = [
|
||||
{ column: 'Lossless', fence: 'markdown', reader: 'losslessMarkdownToAdf', writer: 'adfToLosslessMarkdown' },
|
||||
{ column: 'Portable', fence: 'markdown', reader: 'portableMarkdownToAdf', writer: 'adfToPortableMarkdown' },
|
||||
{ column: 'HTML', fence: 'html', reader: 'htmlToAdf', writer: 'adfToHtml' },
|
||||
]
|
||||
|
||||
const intro = `# What each call writes
|
||||
|
||||
\`feature-table/build.mjs\` writes this page from the corpus and the built package, and \`./ci.sh\` fails where the page differs.
|
||||
|
||||
Each output is read back by its format's reader. \`holds\` means that gives the same document, \`loses\` a different one, and \`refused\` an error whose code the example names.
|
||||
`
|
||||
|
||||
// `library` holds the package's calls, so a test can hand in what a real document cannot make them do.
|
||||
export function renderFeatureTable(examples, library) {
|
||||
const written = examples.map(({ doc, name }) => ({ doc, name, outcomes: writers.map((writer) => outcome(doc, writer, library)) }))
|
||||
const rows = written.map(({ name, outcomes }) => `| [${name}](#${name.toLowerCase().replaceAll(' ', '-')}) | ${outcomes.map(({ result }) => result).join(' | ')} |`)
|
||||
const head = `| Example | ${writers.map(({ column }) => column).join(' | ')} |\n| --- | --- | --- | --- |`
|
||||
return [intro, [head, ...rows].join('\n') + '\n', ...written.map(section)].join('\n')
|
||||
}
|
||||
|
||||
function outcome(doc, { fence, reader, writer }, library) {
|
||||
const output = library[writer](doc)
|
||||
if (!output.ok) return { label: `\`${writer}\` refuses with \`${output.error.code}\`.\n`, result: 'refused' }
|
||||
const read = library[reader](output.value)
|
||||
if (!read.ok) return { label: `\`${writer}\`, round-trip refused with \`${read.error.code}\`:\n\n${fenced(fence, output.value)}`, result: 'refused' }
|
||||
const result = isDeepStrictEqual(read.value, doc) ? 'holds' : 'loses'
|
||||
return { label: `\`${writer}\`, round-trip ${result}:\n\n${fenced(fence, output.value)}`, result }
|
||||
}
|
||||
|
||||
function section({ doc, name, outcomes }) {
|
||||
return [`## ${name}\n`, `ADF:\n\n${fenced('json', `${JSON.stringify(doc)}\n`)}`, ...outcomes.map(({ label }) => label)].join('\n')
|
||||
}
|
||||
|
||||
function fenced(language, text) {
|
||||
const longest = Math.max(2, ...[...text.matchAll(/`+/g)].map(([run]) => run.length))
|
||||
const fence = '`'.repeat(longest + 1)
|
||||
return `${fence}${language}\n${text}${fence}\n`
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
|
||||
import { renderFeatureTable } from './render.mjs'
|
||||
|
||||
const doc = { content: [], type: 'doc', version: 1 }
|
||||
const held = { ok: true, value: doc }
|
||||
const refused = { error: { code: 'unsupported-node-shape' }, ok: false }
|
||||
|
||||
function library(overrides) {
|
||||
const writes = (value) => () => ({ ok: true, value })
|
||||
return { adfToHtml: writes('<p>a</p>\n'), adfToLosslessMarkdown: writes('a\n'), adfToPortableMarkdown: writes('a\n'), htmlToAdf: () => held, losslessMarkdownToAdf: () => held, portableMarkdownToAdf: () => held, ...overrides }
|
||||
}
|
||||
|
||||
test('leads with a row per example linking its section, then the section labels each output', () => {
|
||||
const page = renderFeatureTable([{ doc, name: 'Some example' }], library())
|
||||
assert.ok(page.includes('| [Some example](#some-example) | holds | holds | holds |\n'))
|
||||
assert.ok(page.includes(`## Some example\n\nADF:\n\n\`\`\`json\n${JSON.stringify(doc)}\n\`\`\`\n\n\`adfToLosslessMarkdown\`, round-trip holds:\n\n\`\`\`markdown\na\n\`\`\`\n`))
|
||||
assert.ok(page.includes('`adfToHtml`, round-trip holds:\n\n```html\n<p>a</p>\n```\n'))
|
||||
})
|
||||
|
||||
test('says the round-trip loses where the output reads back to another document', () => {
|
||||
const page = renderFeatureTable([{ doc, name: 'Some example' }], library({ portableMarkdownToAdf: () => ({ ok: true, value: { ...doc, content: [{ type: 'rule' }] } }) }))
|
||||
assert.ok(page.includes('| holds | loses | holds |'))
|
||||
assert.ok(page.includes('`adfToPortableMarkdown`, round-trip loses:'))
|
||||
})
|
||||
|
||||
test('compares documents whatever their key order', () => {
|
||||
const page = renderFeatureTable([{ doc, name: 'Some example' }], library({ htmlToAdf: () => ({ ok: true, value: { version: 1, type: 'doc', content: [] } }) }))
|
||||
assert.ok(page.includes('| holds | holds | holds |'))
|
||||
})
|
||||
|
||||
test('names the code where a writer or a reader refuses', () => {
|
||||
const page = renderFeatureTable([{ doc, name: 'Some example' }], library({ adfToHtml: () => refused, losslessMarkdownToAdf: () => refused }))
|
||||
assert.ok(page.includes('| refused | holds | refused |'))
|
||||
assert.ok(page.includes('`adfToLosslessMarkdown`, round-trip refused with `unsupported-node-shape`:\n\n```markdown\na\n```\n'))
|
||||
assert.ok(page.includes('`adfToHtml` refuses with `unsupported-node-shape`.\n'))
|
||||
})
|
||||
|
||||
test('fences an output in more backticks than it holds', () => {
|
||||
const page = renderFeatureTable([{ doc, name: 'Some example' }], library({ adfToLosslessMarkdown: () => ({ ok: true, value: '````js\nx\n````\n' }) }))
|
||||
assert.ok(page.includes('`````markdown\n````js\nx\n````\n`````\n'))
|
||||
})
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source ./docker-runner.sh
|
||||
|
||||
in_image_memory=4g
|
||||
leg "install ($node_image)" in_image "$node_image" npm ci
|
||||
leg "build ($node_image)" in_image "$node_image" npm run build
|
||||
leg "write the feature table ($node_image)" in_image "$node_image" node feature-table/build.mjs
|
||||
Reference in New Issue
Block a user