diff --git a/.oxlintrc.json b/.oxlintrc.json index 48131ae..32f6278 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -3,22 +3,40 @@ "categories": { "correctness": "off" }, "ignorePatterns": ["src/**/*.test.ts", "src/conformance/property-harness.ts"], "overrides": [ + { + "files": ["src/adf/**"], + "rules": { + "no-restricted-imports": ["error", { "patterns": [{ "group": ["**/html/**", "**/markdown/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }] + } + }, + { + "files": ["src/html/**"], + "rules": { + "no-restricted-imports": ["error", { "patterns": [{ "group": ["**/markdown/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }] + } + }, + { + "files": ["src/markdown/**"], + "rules": { + "no-restricted-imports": ["error", { "patterns": [{ "group": ["**/html/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }] + } + }, { "files": ["src/markdown/directive/**"], "rules": { - "no-restricted-imports": ["error", { "patterns": [{ "group": ["**/emit/**", "**/parse/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }] + "no-restricted-imports": ["error", { "patterns": [{ "group": ["**/emit/**", "**/html/**", "**/parse/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }] } }, { "files": ["src/markdown/emit/**"], "rules": { - "no-restricted-imports": ["error", { "patterns": [{ "group": ["**/portable/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }] + "no-restricted-imports": ["error", { "patterns": [{ "group": ["**/html/**", "**/portable/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }] } }, { "files": ["src/markdown/parse/**"], "rules": { - "no-restricted-imports": ["error", { "patterns": [{ "group": ["**/emit/**", "!../emit/spelling-questions.ts", "**/portable/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }] + "no-restricted-imports": ["error", { "patterns": [{ "group": ["**/emit/**", "!../emit/spelling-questions.ts", "**/html/**", "**/portable/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }] } } ], diff --git a/ci.sh b/ci.sh index 5639d85..c18296f 100755 --- a/ci.sh +++ b/ci.sh @@ -9,26 +9,32 @@ leg "lint ($node_image)" in_image "$node_image" npm run lint # The boundaries pass by finding nothing, so probe that each still fires: oxlint bumps automerge on green. boundaries_fire() { - local allowed=src/markdown/parse/boundary-probe-allowed.ts directive=src/markdown/directive/boundary-probe.ts directive_emit=src/markdown/directive/boundary-probe-emit.ts emit=src/markdown/emit/boundary-probe.ts lint_log parse=src/markdown/parse/boundary-probe.ts parse_portable=src/markdown/parse/boundary-probe-portable.ts + local adf=src/adf/boundary-probe.ts adf_html=src/adf/boundary-probe-html.ts allowed=src/markdown/parse/boundary-probe-allowed.ts directive=src/markdown/directive/boundary-probe.ts directive_emit=src/markdown/directive/boundary-probe-emit.ts directive_html=src/markdown/directive/boundary-probe-html.ts emit=src/markdown/emit/boundary-probe.ts emit_html=src/markdown/emit/boundary-probe-html.ts html=src/html/boundary-probe.ts lint_log markdown=src/markdown/boundary-probe-html.ts parse=src/markdown/parse/boundary-probe.ts parse_html=src/markdown/parse/boundary-probe-html.ts parse_portable=src/markdown/parse/boundary-probe-portable.ts + local probes="$adf $adf_html $allowed $directive $directive_emit $directive_html $emit $emit_html $html $markdown $parse $parse_html $parse_portable" lint_log=$(mktemp) && # The paths are baked in: the trap fires after this function's locals are gone. - trap "rm -f $allowed $directive $directive_emit $emit $lint_log $parse $parse_portable" EXIT && + trap "rm -f $probes $lint_log" EXIT && trap 'exit 130' INT && trap 'exit 143' TERM && echo "import '../emit/spelling-questions.ts';" > "$allowed" && in_image "$node_image" npm run lint && + echo "import '../markdown/flavour.ts';" > "$adf" && + echo "import '../html/emit/adf-to-html.ts';" > "$adf_html" && echo "import '../parse/blocks.ts';" > "$directive" && echo "import '../emit/image.ts';" > "$directive_emit" && + echo "import '../../html/emit/adf-to-html.ts';" > "$directive_html" && echo "import '../portable/conventions.ts';" > "$emit" && + echo "import '../../html/emit/adf-to-html.ts';" > "$emit_html" && + echo "import '../markdown/flavour.ts';" > "$html" && + echo "import '../html/emit/adf-to-html.ts';" > "$markdown" && echo "import '../emit/adf-to-markdown.ts';" > "$parse" && + echo "import '../../html/emit/adf-to-html.ts';" > "$parse_html" && echo "import '../portable/conventions.ts';" > "$parse_portable" && ! in_image "$node_image" npm run lint -- --format unix 2>&1 | tee "$lint_log" && - grep -q "^$directive:.*eslint(no-restricted-imports)" "$lint_log" && - grep -q "^$directive_emit:.*eslint(no-restricted-imports)" "$lint_log" && - grep -q "^$emit:.*eslint(no-restricted-imports)" "$lint_log" && - grep -q "^$parse:.*eslint(no-restricted-imports)" "$lint_log" && - grep -q "^$parse_portable:.*eslint(no-restricted-imports)" "$lint_log" && - rm -f "$allowed" "$directive" "$directive_emit" "$emit" "$lint_log" "$parse" "$parse_portable" + for probe in $probes; do + [ "$probe" = "$allowed" ] || grep -q "^$probe:.*eslint(no-restricted-imports)" "$lint_log" || return 1 + done && + rm -f $probes "$lint_log" } leg "import boundaries fire ($node_image)" boundaries_fire diff --git a/docs/decisions.md b/docs/decisions.md index e5eaeff..8df444e 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -968,7 +968,8 @@ spelling the emitter refuses gives its own error rather than a second name for i is the only import `parse/` takes from `emit/`, through `emit/spelling-questions.ts`. `.oxlintrc.json` enforces that limit for `markdown/parse/`, keeps `parse/` and `emit/` from -importing `markdown/portable/`, and keeps `markdown/directive/` from importing `emit/` or `parse/`, -as `no-restricted-imports` overrides. They are lint rules because the technical principles refuse a +importing `markdown/portable/`, keeps `markdown/directive/` from importing `emit/` or `parse/`, +keeps `markdown/` and `html/` from importing each other, and keeps `adf/` from importing either, as +`no-restricted-imports` overrides. They are lint rules because the technical principles refuse a test that scans the source, and a lint rule reads every import spelling: `from`, a bare `import`, double quotes and a dynamic `import()`. diff --git a/spec/html.md b/spec/html.md index 71f564e..ab89c44 100644 --- a/spec/html.md +++ b/spec/html.md @@ -49,7 +49,9 @@ its scheme). A node the sections above cannot write where it stands is the carry element: `
html(adfToHtml(document({ attrs: { layout: 'center' }, content: [media], type: 'mediaSingle' }))).startsWith(' ${element} ${anchor}${element}
' : undefined)
- return text === undefined ? undefined : success(text)
+ const html = tryCodeBlock(node) ?? tryImage(node) ?? (node.type === 'rule' && holdsOnlyAttributes(node, []) && nodeContent(node).length === 0 ? '
' : undefined)
+ return html === undefined ? undefined : success(html)
}
-function inlineElement(tag: string, attributes: string, nodes: readonly AdfNode[], path: ConvertErrorPath): Result
`
const [only, ...others] = node.content
- if (only === undefined || others.length > 0 || !writableText(only) || !isUnmarkedSpellableText(only)) return undefined
- return `
`
+ const code = only === undefined || others.length > 0 || !isUnmarkedSpellableText(only) ? undefined : writableText(only.text)
+ return code === undefined ? undefined : `${escapeHtml(only.text)}
`
}
function tryImage(node: AdfNode): string | undefined {
const image = node.type === 'mediaSingle' ? plainImage(node) : undefined
- if (image === undefined || !writableUrl(image.url) || (image.alt !== undefined && image.alt.includes('\0'))) return undefined
- const element = `${code}`
+ if (image === undefined) return undefined
+ const src = htmlValue(image.url)
+ const alt = optionalAttribute('alt', image.alt)
+ if (src === undefined || alt === undefined) return undefined
+ const element = `
`
if (image.link === undefined) return `
' : undefined
- if (!isSpellableText(node) || !writableText(node)) return undefined
- const marks = nodeMarks(node)
- const openers: string[] = []
- const closers: string[] = []
- for (const mark of marks) {
- if (marks.filter(({ type }) => type === mark.type).length > 1) return undefined
- const element = markElement(mark)
- if (element === undefined) return undefined
- openers.push(element.opener)
- closers.unshift(element.closer)
- }
- return `${openers.join('')}${escapeHtml(node.text)}${closers.join('')}`
-}
-
-function markElement(mark: AdfMark): { closer: string; opener: string } | undefined {
- const { type } = mark
- if (elementMarks.includes(type)) return mark.attrs === undefined ? { closer: `${type}>`, opener: `<${type}>` } : undefined
- if (type !== 'link' || Object.keys(nodeAttrs(mark)).some((key) => key !== 'href' && key !== 'title')) return undefined
- const target = linkTarget(nodeAttrs(mark))
- const opener = target === undefined ? undefined : anchorOpener(target)
- return opener === undefined ? undefined : { closer: '', opener }
-}
-
-function anchorOpener({ href, title }: LinkTarget): string | undefined {
- if (!writableUrl(href) || title?.includes('\0') === true) return undefined
- return ``
-}
-
-// HTML holds no null character, and no element holds empty text apart from no text.
-function writableText(node: AdfNode): node is AdfNode & { text: string } {
- return typeof node.text === 'string' && node.text !== '' && !node.text.includes('\0')
-}
-
-// Any scheme, `javascript:` included, as the ADF holds it: docs/decisions.md §HTML writes a URL as the ADF holds it, whatever its scheme.
-function writableUrl(url: string): boolean {
- return !url.includes('\0')
-}
-
-function carriedBlock(node: AdfNode, path: ConvertErrorPath, depth: number): Result
' : undefined
+ const text = isSpellableText(node) ? writableText(node.text) : undefined
+ if (text === undefined) return undefined
+ const marks = nodeMarks(node)
+ const openers: string[] = []
+ const closers: string[] = []
+ for (const mark of marks) {
+ if (marks.filter(({ type }) => type === mark.type).length > 1) return undefined
+ const element = markElement(mark)
+ if (element === undefined) return undefined
+ openers.push(element.opener)
+ closers.unshift(element.closer)
+ }
+ return `${openers.join('')}${text}${closers.join('')}`
+}
+
+// No element holds empty text apart from no text.
+export function writableText(text: string | undefined): string | undefined {
+ return text === undefined || text === '' ? undefined : htmlValue(text)
+}
+
+function markElement(mark: AdfMark): { closer: string; opener: string } | undefined {
+ const { type } = mark
+ if (elementMarks.includes(type)) return mark.attrs === undefined ? { closer: `${type}>`, opener: `<${type}>` } : undefined
+ if (type !== 'link' || Object.keys(nodeAttrs(mark)).some((key) => key !== 'href' && key !== 'title')) return undefined
+ const link = linkAttrs(nodeAttrs(mark))
+ const opener = link === undefined ? undefined : anchorOpener(link)
+ return opener === undefined ? undefined : { closer: '