5c: the build, the freeze audit and the release pipeline
This commit is contained in:
@@ -45,6 +45,11 @@ const orderFault = 'the {attrs} keys read in alphabetical order'
|
||||
const pairFault = 'an attribute reads key=value, the value bare or double-quoted: this one does not'
|
||||
const shapeFault = `a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; ${directiveLineEscape}`
|
||||
|
||||
export function attributeNestingFault(text: string, kind: AttributeKind, key: string, type: string): ConvertFault | undefined {
|
||||
if (kind !== 'json' || parseJson(text, Number.POSITIVE_INFINITY) === undefined) return undefined
|
||||
return { code: 'unsupported-nesting-depth', message: `the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels the parser carries` }
|
||||
}
|
||||
|
||||
export function attributeValue(text: string, kind: AttributeKind): VocabularyValue | undefined {
|
||||
if (kind === 'string') return { kind, value: text }
|
||||
if (kind === 'boolean') return text === 'true' || text === 'false' ? { kind, value: text === 'true' } : undefined
|
||||
@@ -294,10 +299,10 @@ function readQuotedValue(text: string, index: number): Read<{ end: number; value
|
||||
return { value: { end: cursor + 1, value: { decoded: parsed, spelling } } }
|
||||
}
|
||||
|
||||
function parseJson(raw: string): JsonValue | undefined {
|
||||
function parseJson(raw: string, levels: number = largestNesting): JsonValue | undefined {
|
||||
try {
|
||||
const value: unknown = JSON.parse(raw)
|
||||
return isJsonValue(value) ? value : undefined
|
||||
return isJsonValue(value, levels) ? value : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { JsonValue } from '../../json-value.ts'
|
||||
import type { Result } from '../../result.ts'
|
||||
import { adfToMarkdown } from '../../index.ts'
|
||||
import { adfToMarkdown, markdownToAdf } from '../../index.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
|
||||
function document(...content: AdfNode[]): AdfDocument {
|
||||
return { content, type: 'doc', version: 1 }
|
||||
@@ -334,10 +336,18 @@ test('refuses marks and attributes nested deeper than the emitter carries', () =
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ marks, text: 'x', type: 'text' })))), 'unsupported-nesting-depth')
|
||||
let attrs: AdfMark['attrs'] = { depth: 'x' }
|
||||
for (let depth = 0; depth < 600; depth += 1) attrs = { depth: attrs }
|
||||
assert.equal(
|
||||
markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))),
|
||||
"not-an-adf-document: an ADF document's content holds ADF nodes: one of them is not",
|
||||
)
|
||||
const deeper = `unsupported-nesting-depth: an attribute value nests deeper than the ${largestNesting} levels the emitter carries`
|
||||
assert.equal(markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), deeper)
|
||||
const card = (levels: number): AdfNode => {
|
||||
let data: JsonValue = 1
|
||||
for (let level = 0; level < levels; level += 1) data = [data]
|
||||
return { attrs: { data, url: 'https://example.com/a' }, type: 'inlineCard' }
|
||||
}
|
||||
assert.equal(markdown(adfToMarkdown(document(paragraph(card(largestNesting + 1))))), deeper)
|
||||
assert.deepEqual(path(adfToMarkdown(document(paragraph(card(largestNesting + 1))))), [])
|
||||
const spelled = adfToMarkdown(document(paragraph(card(largestNesting))))
|
||||
assert.ok(spelled.ok, spelled.ok ? '' : spelled.error.message)
|
||||
assert.deepEqual(markdownToAdf(spelled.value), { ok: true, value: document(paragraph(card(largestNesting))) })
|
||||
})
|
||||
|
||||
test('escapes a literal delimiter that would merge with an emitted one', () => {
|
||||
|
||||
@@ -24,6 +24,9 @@ const largestListMarker = 999999999
|
||||
|
||||
export function adfToMarkdown(document: AdfDocument): Result<string> {
|
||||
const fault = adfDocumentFault(document)
|
||||
if (fault !== undefined && adfDocumentFault(document, Number.POSITIVE_INFINITY) === undefined) {
|
||||
return failure('unsupported-nesting-depth', `an attribute value nests deeper than the ${largestNesting} levels the emitter carries`, [])
|
||||
}
|
||||
if (fault !== undefined) return failure('not-an-adf-document', fault, [])
|
||||
if (document.version !== 1) return failure('unsupported-document-version', `no markdown spelling carries ADF version ${document.version}`, [])
|
||||
const blocks = emitBlocks(document.content ?? [], 'document', [], 0)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AdfAttributes } from '../../adf/document.ts'
|
||||
import type { AttributeVocabulary } from '../../adf/attribute-vocabulary.ts'
|
||||
import type { DirectiveAttributes } from '../directive-syntax.ts'
|
||||
import { attributeValue, spellAttributeValue } from '../directive-syntax.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { attributeNestingFault, attributeValue, spellAttributeValue } from '../directive-syntax.ts'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
|
||||
export type Elsewhere = { key: string; slot: 'argument' | 'content' }
|
||||
|
||||
@@ -22,7 +22,11 @@ export function readVocabulary(
|
||||
const kind = Object.hasOwn(vocabulary, key) ? vocabulary[key] : undefined
|
||||
if (kind === undefined) return failure('unsupported-node-shape', `${type} holds no ${key} attribute: this one spells it`, path)
|
||||
const read = attributeValue(spelled.decoded, kind)
|
||||
if (read === undefined) return failure('unsupported-node-shape', `the ${key} attribute of ${type} is no ${kind}`, path)
|
||||
if (read === undefined) {
|
||||
const deep = attributeNestingFault(spelled.decoded, kind, key, type)
|
||||
if (deep !== undefined) return faulted(deep, path)
|
||||
return failure('unsupported-node-shape', `the ${key} attribute of ${type} is no ${kind}`, path)
|
||||
}
|
||||
const spelling = spellAttributeValue(read)
|
||||
if (spelling !== spelled.spelling) return failure('unsupported-node-shape', `${type} spells its ${key} attribute as ${key}=${spelling}`, path)
|
||||
attrs[key] = read.value
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { BlockDirective } from '../../adf/block-directives.ts'
|
||||
import type { ConvertFault } from '../../result.ts'
|
||||
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
|
||||
import type { Elsewhere } from './directive-attributes.ts'
|
||||
import { attributeValue, directiveLineEscape, inlineDirectiveEscape, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||
import { attributeNestingFault, attributeValue, directiveLineEscape, inlineDirectiveEscape, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||
import { blockArgument } from '../block-directive-arguments.ts'
|
||||
import { blockDirective } from '../../adf/block-directives.ts'
|
||||
import { carryName } from '../opaque-carry.ts'
|
||||
@@ -94,6 +94,8 @@ function slotText(content: readonly AdfNode[]): string | undefined {
|
||||
|
||||
function readMarks(type: string, spelled: DirectiveValue, path: ConvertErrorPath): Result<AdfMark[]> {
|
||||
const read = attributeValue(spelled.decoded, 'json')
|
||||
const deep = read === undefined ? attributeNestingFault(spelled.decoded, 'json', marksAttribute, type) : undefined
|
||||
if (deep !== undefined) return faulted(deep, path)
|
||||
const marks = read === undefined || spellAttributeValue(read) !== spelled.spelling ? undefined : readMarkValues(read.value)
|
||||
if (marks === undefined) {
|
||||
return failure('unsupported-node-shape', `the ${marksAttribute} attribute of ${type} is its marks array in canonical JSON: this one is not`, path)
|
||||
|
||||
@@ -431,12 +431,18 @@ test('names the attribute a node holds no reading for', () => {
|
||||
assert.equal(content(markdownToAdf(':::table {isNumberColumnEnabled=yes}\n:::\n')), 'unsupported-node-shape: the isNumberColumnEnabled attribute of table is no boolean')
|
||||
assert.equal(content(markdownToAdf('::media {width=true}\n')), 'unsupported-node-shape: the width attribute of media is no number')
|
||||
assert.equal(content(markdownToAdf(':::tableCell {colwidth="[340,"}\n:::\n')), 'unsupported-node-shape: the colwidth attribute of tableCell is no json')
|
||||
const deep = `${'['.repeat(largestNesting + 2)}${']'.repeat(largestNesting + 2)}`
|
||||
assert.equal(content(markdownToAdf(`:::tableCell {colwidth="${deep}"}\n:::\n`)), 'unsupported-node-shape: the colwidth attribute of tableCell is no json')
|
||||
assert.equal(content(markdownToAdf(':::panel info {panelType=note}\nx\n:::\n')), 'unsupported-node-shape: panel spells its panelType attribute as the directive argument, never in {attrs}')
|
||||
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot, never in {attrs}')
|
||||
})
|
||||
|
||||
test('names the depth an attribute value nests past, never the kind the JSON reads as', () => {
|
||||
const nested = (levels: number): string => `${'['.repeat(levels)}1${']'.repeat(levels)}`
|
||||
const deeper = (key: string, type: string): string => `unsupported-nesting-depth: the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels the parser carries`
|
||||
assert.equal(content(markdownToAdf(`:::tableCell {colwidth="${nested(largestNesting + 1)}"}\n:::\n`)), deeper('colwidth', 'tableCell'))
|
||||
assert.equal(content(markdownToAdf(`::rule {marks="${nested(largestNesting + 1)}"}\n`)), deeper('marks', 'rule'))
|
||||
assert.equal(content(markdownToAdf(`::media {width="${nested(largestNesting + 1)}"}\n`)), 'unsupported-node-shape: the width attribute of media is no number')
|
||||
})
|
||||
|
||||
test('names the attribute value spelled outside the canonical form', () => {
|
||||
assert.equal(content(markdownToAdf('::rule {localId="a-1"}\n')), 'unsupported-node-shape: rule spells its localId attribute as localId=a-1')
|
||||
assert.equal(content(markdownToAdf('::media {width="20.0"}\n')), 'unsupported-node-shape: media spells its width attribute as width=20')
|
||||
|
||||
Reference in New Issue
Block a user