12c: the block directive forms read and spell !adf: openers and closers
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { blockDirective } from '../adf/block-directives.ts'
|
||||
import { listBreakName } from './list-break.ts'
|
||||
|
||||
export function blockDirectiveForm(name: string): 'container' | 'leaf' | undefined {
|
||||
if (name === listBreakName) return 'leaf'
|
||||
const directive = blockDirective(name)
|
||||
if (directive === undefined) return undefined
|
||||
return directive.contentModel === 'none' ? 'leaf' : 'container'
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { JsonValue } from '../json-value.ts'
|
||||
import { carryFence } from './opaque-carry.ts'
|
||||
import { carryName } from './opaque-carry.ts'
|
||||
import { holdsControlCharacter } from './commonmark-grammar.ts'
|
||||
import { holdsEntityReference } from './entity-references.ts'
|
||||
|
||||
@@ -8,7 +8,7 @@ export type LanguageSlot = { info: string; kind: 'fence' } | { kind: 'attribute'
|
||||
// spec/flavour.md, The CommonMark blocks: the one slot a codeBlock's language rides.
|
||||
export function languageSlot(language: JsonValue | undefined): LanguageSlot {
|
||||
if (language === undefined) return { kind: 'none' }
|
||||
if (typeof language !== 'string' || language === '' || language === carryFence) return { kind: 'attribute' }
|
||||
if (typeof language !== 'string' || language === '' || language === carryName) return { kind: 'attribute' }
|
||||
if (/[`\\]/.test(language) || holdsControlCharacter(language) || language !== language.trim() || holdsEntityReference(language)) return { kind: 'attribute' }
|
||||
return { info: language, kind: 'fence' }
|
||||
}
|
||||
|
||||
@@ -53,11 +53,10 @@ const asciiPunctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/
|
||||
const atxHeadingOpener = /^(#{1,6})(?:[ \t]|$)/
|
||||
const blankLine = /^[ \t]*$/
|
||||
const codeFenceOpener = /^(`{3,}|~{3,})/
|
||||
const directiveClaim = /^:{2,}(?:[A-Za-z0-9]|[ \t]*$)/
|
||||
const pipeClaim = /^\|/
|
||||
const bulletListOpener = /^[*+-](?:[ \t]|$)/
|
||||
// A superset of what the parser claims: over-escaping a line is safe, under-escaping one breaks the round-trip.
|
||||
const firstCharacterOpeners = [atxHeadingOpener, /^>/, bulletListOpener, codeFenceOpener, /^:{2,}/, pipeClaim]
|
||||
const firstCharacterOpeners = [atxHeadingOpener, /^>/, bulletListOpener, codeFenceOpener, pipeClaim]
|
||||
// CommonMark 0.31.2, Autolinks: the email production, whose label may not open or close with a hyphen.
|
||||
const emailNameSource = "[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+"
|
||||
const emailLabelSource = '[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?'
|
||||
@@ -81,10 +80,6 @@ export function backslashEscape(text: string, index: number): string | undefined
|
||||
return isAsciiPunctuation(escaped) ? escaped : undefined
|
||||
}
|
||||
|
||||
export function claimsDirectiveLine(line: string): boolean {
|
||||
return directiveClaim.test(line)
|
||||
}
|
||||
|
||||
export function claimsLine(line: string, position: LinePosition): boolean {
|
||||
return escapesLineClaim(line, 0, position) || orderedListOpener.test(line)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ function attributes(...pairs: Pair[]): DirectiveAttributes {
|
||||
return new Map(pairs.map(([key, decoded, spelling]) => [key, { decoded, spelling: spelling ?? decoded }]))
|
||||
}
|
||||
|
||||
function header(colons: number, name: string, argument?: string, ...pairs: Pair[]): { value: DirectiveLine } {
|
||||
return { value: { argument, attributes: attributes(...pairs), colons, kind: 'header', name } }
|
||||
function opener(name: string, argument?: string, ...pairs: Pair[]): { value: DirectiveLine } {
|
||||
return { value: { argument, attributes: attributes(...pairs), kind: 'opener', name } }
|
||||
}
|
||||
|
||||
function fault(line: string): string {
|
||||
@@ -32,80 +32,87 @@ function spans(text: string, name: string, content: string | undefined, ...pairs
|
||||
assert.deepEqual(inline(text), { attributes: attributes(...pairs), content, length: text.length, name })
|
||||
}
|
||||
|
||||
test('claims a colon-run line only where a name or nothing follows the colons', () => {
|
||||
test('claims a prefixed line only where a closer, or a name a space or the line end follows, opens it', () => {
|
||||
assert.equal(readDirectiveLine('Part.'), undefined)
|
||||
assert.equal(readDirectiveLine(':: two'), undefined)
|
||||
assert.equal(readDirectiveLine(':panel'), undefined)
|
||||
assert.equal(readDirectiveLine(' ::rule'), undefined)
|
||||
assert.equal(readDirectiveLine(':::panel info'), undefined)
|
||||
assert.equal(readDirectiveLine(' !adf:rule'), undefined)
|
||||
assert.equal(readDirectiveLine('!adf:mention[@A]'), undefined)
|
||||
assert.equal(readDirectiveLine('!adf:hardBreak{}'), undefined)
|
||||
assert.equal(readDirectiveLine('!adf:panel{}'), undefined)
|
||||
assert.equal(readDirectiveLine('!adf:'), undefined)
|
||||
assert.equal(readDirectiveLine('!adf:/'), undefined)
|
||||
assert.equal(readDirectiveLine('!adf:Panel'), undefined)
|
||||
assert.equal(readDirectiveLine('!adf:panel\tinfo'), undefined)
|
||||
})
|
||||
|
||||
test('reads a bare colon run as the fence that closes a container', () => {
|
||||
assert.deepEqual(readDirectiveLine(':::'), { value: { colons: 3, kind: 'closing' } })
|
||||
assert.deepEqual(readDirectiveLine('::'), { value: { colons: 2, kind: 'closing' } })
|
||||
assert.deepEqual(readDirectiveLine(':::::: \t'), { value: { colons: 6, kind: 'closing' } })
|
||||
test('reads a closer as the name it closes, and nothing after the name', () => {
|
||||
assert.deepEqual(readDirectiveLine('!adf:/panel'), { value: { kind: 'closer', name: 'panel' } })
|
||||
assert.deepEqual(readDirectiveLine('!adf:/panel \t'), { value: { kind: 'closer', name: 'panel' } })
|
||||
const closer = 'a closer carries nothing after its name: this one does; \\!adf: keeps the prefix literal'
|
||||
assert.equal(fault('!adf:/panel info'), closer)
|
||||
assert.equal(fault('!adf:/panel{}'), closer)
|
||||
})
|
||||
|
||||
test('reads the leaf and container forms, their argument and their attributes', () => {
|
||||
assert.deepEqual(readDirectiveLine('::rule'), header(2, 'rule'))
|
||||
assert.deepEqual(readDirectiveLine('::rule '), header(2, 'rule'))
|
||||
assert.deepEqual(readDirectiveLine('::taskItem TODO'), header(2, 'taskItem', 'TODO'))
|
||||
assert.deepEqual(readDirectiveLine('::media {id=a-1 type=file}'), header(2, 'media', undefined, ['id', 'a-1'], ['type', 'file']))
|
||||
assert.deepEqual(readDirectiveLine('::panel info {panelColor="#ff0000"} '), header(2, 'panel', 'info', ['panelColor', '#ff0000', '"#ff0000"']))
|
||||
assert.deepEqual(readDirectiveLine(':::panel info'), header(3, 'panel', 'info'))
|
||||
test('reads the opener, its argument and its attributes', () => {
|
||||
assert.deepEqual(readDirectiveLine('!adf:rule'), opener('rule'))
|
||||
assert.deepEqual(readDirectiveLine('!adf:rule \t'), opener('rule'))
|
||||
assert.deepEqual(readDirectiveLine('!adf:rule\t'), opener('rule'))
|
||||
assert.deepEqual(readDirectiveLine('!adf:taskItem TODO'), opener('taskItem', 'TODO'))
|
||||
assert.deepEqual(readDirectiveLine('!adf:media {id=a-1 type=file}'), opener('media', undefined, ['id', 'a-1'], ['type', 'file']))
|
||||
assert.deepEqual(readDirectiveLine('!adf:panel info {panelColor="#ff0000"} '), opener('panel', 'info', ['panelColor', '#ff0000', '"#ff0000"']))
|
||||
})
|
||||
|
||||
test('decodes a quoted attribute value, the escapes {attrs} reserves included', () => {
|
||||
assert.deepEqual(readDirectiveLine('::extension {text="two words"}'), header(2, 'extension', undefined, ['text', 'two words', '"two words"']))
|
||||
assert.deepEqual(readDirectiveLine('!adf:extension {text="two words"}'), opener('extension', undefined, ['text', 'two words', '"two words"']))
|
||||
assert.deepEqual(
|
||||
readDirectiveLine('::extension {text="a\\u0060b\\u0026c\\u003cd\\u007ce"}'),
|
||||
header(2, 'extension', undefined, ['text', 'a`b&c<d|e', '"a\\u0060b\\u0026c\\u003cd\\u007ce"']),
|
||||
readDirectiveLine('!adf:extension {text="a\\u0060b\\u0026c\\u003cd\\u007ce"}'),
|
||||
opener('extension', undefined, ['text', 'a`b&c<d|e', '"a\\u0060b\\u0026c\\u003cd\\u007ce"']),
|
||||
)
|
||||
assert.deepEqual(readDirectiveLine('::extension {text="a\\"b\\\\c\\nd"}'), header(2, 'extension', undefined, ['text', 'a"b\\c\nd', '"a\\"b\\\\c\\nd"']))
|
||||
assert.deepEqual(readDirectiveLine('::extension {text="}{"}'), header(2, 'extension', undefined, ['text', '}{', '"}{"']))
|
||||
assert.deepEqual(readDirectiveLine('!adf:extension {text="a\\"b\\\\c\\nd"}'), opener('extension', undefined, ['text', 'a"b\\c\nd', '"a\\"b\\\\c\\nd"']))
|
||||
assert.deepEqual(readDirectiveLine('!adf:extension {text="}{"}'), opener('extension', undefined, ['text', '}{', '"}{"']))
|
||||
})
|
||||
|
||||
test('names the {attrs} keys read out of the alphabetical order canonical form spells', () => {
|
||||
assert.equal(fault('::media {type=file id=a-1}'), 'the {attrs} keys read in alphabetical order: id before type')
|
||||
assert.deepEqual(readDirectiveLine('::media {id=a-1 type=file}'), header(2, 'media', undefined, ['id', 'a-1'], ['type', 'file']))
|
||||
assert.equal(fault('!adf:media {type=file id=a-1}'), 'the {attrs} keys read in alphabetical order: id before type')
|
||||
assert.deepEqual(readDirectiveLine('!adf:media {id=a-1 type=file}'), opener('media', undefined, ['id', 'a-1'], ['type', 'file']))
|
||||
})
|
||||
|
||||
test('spells an empty {attrs} only where the brace itself claims the directive', () => {
|
||||
const omitted = 'an empty {attrs} is omitted unless the { itself claims the directive: this one spells {}'
|
||||
assert.equal(fault('::rule {}'), omitted)
|
||||
assert.equal(fault(':::panel info {}'), omitted)
|
||||
assert.equal(fault('!adf:rule {}'), omitted)
|
||||
assert.equal(fault('!adf:panel info {}'), omitted)
|
||||
assert.equal(inline('!adf:underline[a]{}'), omitted)
|
||||
spans('!adf:hardBreak{}', 'hardBreak', undefined)
|
||||
})
|
||||
|
||||
test('names the directive line no spelling reads', () => {
|
||||
assert.equal(fault('::Panel'), 'a directive name reads [a-z][A-Za-z0-9]*: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::1panel'), 'a directive name reads [a-z][A-Za-z0-9]*: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel info'), 'a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel info extra'), 'a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel{}'), 'a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel info{}'), 'a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel {a=1} x'), 'a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; \\::: keeps the line literal text')
|
||||
test('names the opener no spelling reads', () => {
|
||||
const shape = 'a directive opener reads a name, one bare argument and {attrs}, one space apart: this one does not; \\!adf: keeps the prefix literal'
|
||||
assert.equal(fault('!adf:panel info'), shape)
|
||||
assert.equal(fault('!adf:panel info extra'), shape)
|
||||
assert.equal(fault('!adf:panel "info"'), shape)
|
||||
assert.equal(fault('!adf:panel info{}'), shape)
|
||||
assert.equal(fault('!adf:panel {a=1} x'), shape)
|
||||
})
|
||||
|
||||
test('names the attributes no spelling reads', () => {
|
||||
assert.equal(fault('::panel {a=1'), 'the {attrs} closing brace is missing')
|
||||
assert.equal(fault('::panel {a="x}'), 'the {attrs} quoted value is unclosed')
|
||||
assert.equal(fault('::panel {a="\\uzzzz"}'), 'the {attrs} quoted value is not a JSON string')
|
||||
assert.equal(fault('::panel {a}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel {a=}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel {=1}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel {a=1 b=2}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel { a=1}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel {a=1 }'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel {a=1 a=2}'), 'the attribute key a is spelled twice')
|
||||
assert.equal(fault('!adf:panel {a=1'), 'the {attrs} closing brace is missing')
|
||||
assert.equal(fault('!adf:panel {a="x}'), 'the {attrs} quoted value is unclosed')
|
||||
assert.equal(fault('!adf:panel {a="\\uzzzz"}'), 'the {attrs} quoted value is not a JSON string')
|
||||
assert.equal(fault('!adf:panel {a}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\!adf: keeps the prefix literal')
|
||||
assert.equal(fault('!adf:panel {a=}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\!adf: keeps the prefix literal')
|
||||
assert.equal(fault('!adf:panel {=1}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\!adf: keeps the prefix literal')
|
||||
assert.equal(fault('!adf:panel {a=1 b=2}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\!adf: keeps the prefix literal')
|
||||
assert.equal(fault('!adf:panel { a=1}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\!adf: keeps the prefix literal')
|
||||
assert.equal(fault('!adf:panel {a=1 }'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\!adf: keeps the prefix literal')
|
||||
assert.equal(fault('!adf:panel {a=1 a=2}'), 'the attribute key a is spelled twice')
|
||||
assert.equal(inline('!adf:mention[@A]{id}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\!adf: keeps the prefix literal')
|
||||
})
|
||||
|
||||
test('breaks the directive on the raw characters a quoted value spells as escapes', () => {
|
||||
assert.equal(fault('::panel {a="x`y"}'), 'a raw ` inside {attrs} breaks the directive: spell it \\u0060')
|
||||
assert.equal(fault('::panel {a="x&y"}'), 'a raw & inside {attrs} breaks the directive: spell it \\u0026')
|
||||
assert.equal(fault('::panel {a="x<y"}'), 'a raw < inside {attrs} breaks the directive: spell it \\u003c')
|
||||
assert.equal(fault('::panel {a="x|y"}'), 'a raw | inside {attrs} breaks the directive: spell it \\u007c')
|
||||
assert.equal(fault('!adf:panel {a="x`y"}'), 'a raw ` inside {attrs} breaks the directive: spell it \\u0060')
|
||||
assert.equal(fault('!adf:panel {a="x&y"}'), 'a raw & inside {attrs} breaks the directive: spell it \\u0026')
|
||||
assert.equal(fault('!adf:panel {a="x<y"}'), 'a raw < inside {attrs} breaks the directive: spell it \\u003c')
|
||||
assert.equal(fault('!adf:panel {a="x|y"}'), 'a raw | inside {attrs} breaks the directive: spell it \\u007c')
|
||||
})
|
||||
|
||||
test('claims at the prefix, and reads a directive only where a bracket or a brace follows the name', () => {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { AttributeKind, VocabularyPair, VocabularyValue } from '../adf/attribute-vocabulary.ts'
|
||||
import type { ConvertFault } from '../result.ts'
|
||||
import type { JsonValue } from '../json-value.ts'
|
||||
import { backslashEscape, claimsDirectiveLine } from './commonmark-grammar.ts'
|
||||
import { backslashEscape } from './commonmark-grammar.ts'
|
||||
import { backtickRun, closingBacktickRun } from './backtick-runs.ts'
|
||||
import { isJsonValue, overNested } from '../json-value.ts'
|
||||
import { largestNesting } from '../nesting.ts'
|
||||
import { runLength } from './emphasis-matching.ts'
|
||||
import { serializeCanonicalJson } from '../canonical-json.ts'
|
||||
|
||||
export type AttributeReading = { refusal: 'kind' | 'nesting'; value?: undefined } | { refusal?: undefined; value: VocabularyValue }
|
||||
@@ -15,17 +14,19 @@ export type DirectiveValue = { decoded: string; spelling: string }
|
||||
export type DirectiveAttributes = ReadonlyMap<string, DirectiveValue>
|
||||
|
||||
export type DirectiveLine =
|
||||
| { argument: string | undefined; attributes: DirectiveAttributes; colons: number; kind: 'header'; name: string }
|
||||
| { colons: number; kind: 'closing' }
|
||||
| { argument: string | undefined; attributes: DirectiveAttributes; kind: 'opener'; name: string }
|
||||
| { kind: 'closer'; name: string }
|
||||
|
||||
export type DirectiveSpan = { attributes: DirectiveAttributes; content: string | undefined; length: number; name: string }
|
||||
|
||||
export type Read<T> = { fault: ConvertFault; value?: undefined } | { fault?: undefined; value: T }
|
||||
|
||||
type Attributes = { attributes: DirectiveAttributes; length: number }
|
||||
type Attributes = { attributes: DirectiveAttributes; end: number }
|
||||
|
||||
type AttributePair = { end: number; key: string; value: DirectiveValue }
|
||||
|
||||
type Content = { content: string | undefined; end: number }
|
||||
|
||||
export const directivePrefix = '!adf:'
|
||||
|
||||
const bareTokenSource = '[A-Za-z0-9_-]+'
|
||||
@@ -40,15 +41,14 @@ const quotedEscapes = new RegExp(reservedSource, 'g')
|
||||
const rawReserved = new RegExp(reservedSource)
|
||||
const noAttributes: DirectiveAttributes = new Map()
|
||||
|
||||
export const directiveLineEscape = '\\::: keeps the line literal text'
|
||||
export const inlineDirectiveEscape = `\\${directivePrefix} keeps the prefix literal`
|
||||
export const directiveEscape = `\\${directivePrefix} keeps the prefix literal`
|
||||
|
||||
const closerFault = `a closer carries nothing after its name: this one does; ${directiveEscape}`
|
||||
const emptyFault = 'an empty {attrs} is omitted unless the { itself claims the directive: this one spells {}'
|
||||
const nameFault = `a directive name reads [a-z][A-Za-z0-9]*: this one does not; ${directiveLineEscape}`
|
||||
const openerFault = `a directive opener reads a name, one bare argument and {attrs}, one space apart: this one does not; ${directiveEscape}`
|
||||
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 prefixFault = `an unescaped ${directivePrefix} completes no directive; ${inlineDirectiveEscape}`
|
||||
const shapeFault = `a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; ${directiveLineEscape}`
|
||||
const pairFault = `an attribute reads key=value, the value bare or double-quoted: this one does not; ${directiveEscape}`
|
||||
const prefixFault = `an unescaped ${directivePrefix} completes no directive; ${directiveEscape}`
|
||||
|
||||
export function attributeValue(text: string, kind: AttributeKind): AttributeReading {
|
||||
if (kind === 'string') return { value: { kind, value: text } }
|
||||
@@ -76,13 +76,18 @@ export function malformedDirective(message: string): ConvertFault {
|
||||
}
|
||||
|
||||
export function readDirectiveLine(line: string): Read<DirectiveLine> | undefined {
|
||||
if (!claimsDirectiveLine(line)) return undefined
|
||||
const colons = runLength(line, 0)
|
||||
const rest = line.slice(colons)
|
||||
if (lineEnd.test(rest)) return { value: { colons, kind: 'closing' } }
|
||||
const header = readDirectiveHeader(rest)
|
||||
if (header.fault !== undefined) return { fault: header.fault }
|
||||
return { value: { argument: header.value.argument, attributes: header.value.attributes, colons, kind: 'header', name: header.value.name } }
|
||||
if (!claimsDirectivePrefix(line, 0)) return undefined
|
||||
const closer = line.charAt(directivePrefix.length) === '/'
|
||||
const start = directivePrefix.length + (closer ? 1 : 0)
|
||||
directiveName.lastIndex = start
|
||||
const name = directiveName.exec(line)?.[0]
|
||||
if (name === undefined) return undefined
|
||||
const rest = line.slice(start + name.length)
|
||||
if (closer) return lineEnd.test(rest) ? { value: { kind: 'closer', name } } : { fault: malformedDirective(closerFault) }
|
||||
if (!rest.startsWith(' ') && !lineEnd.test(rest)) return undefined
|
||||
const opener = readOpenerRest(rest)
|
||||
if (opener.fault !== undefined) return { fault: opener.fault }
|
||||
return { value: { argument: opener.value.argument, attributes: opener.value.attributes, kind: 'opener', name } }
|
||||
}
|
||||
|
||||
export function readInlineDirective(text: string, index: number): Read<DirectiveSpan> | undefined {
|
||||
@@ -110,6 +115,10 @@ export function spellAttributes(pairs: readonly (readonly [string, string])[]):
|
||||
return `{${spelled.join(' ')}}`
|
||||
}
|
||||
|
||||
export function spellDirectiveCloser(name: string): string {
|
||||
return `${directivePrefix}/${name}`
|
||||
}
|
||||
|
||||
export function spellJsonAttribute(value: JsonValue): string {
|
||||
return quote(serializeCanonicalJson(value, 'compact'))
|
||||
}
|
||||
@@ -118,7 +127,7 @@ export function spellInlineDirectiveOpener(name: string): string {
|
||||
return `${directivePrefix}${name}[`
|
||||
}
|
||||
|
||||
export function spellLeafDirective(name: string, attributes: string): string {
|
||||
export function spellInlineLeafDirective(name: string, attributes: string): string {
|
||||
return `${directivePrefix}${name}${attributes === '' ? '{}' : attributes}`
|
||||
}
|
||||
|
||||
@@ -137,18 +146,14 @@ export function spellVocabulary(pairs: readonly VocabularyPair[]): [string, stri
|
||||
return pairs.map((pair): [string, string] => [pair.key, spellAttributeValue(pair)])
|
||||
}
|
||||
|
||||
export function unknownDirectiveFault(name: string, escape: string): ConvertFault {
|
||||
return { code: 'unknown-directive-name', message: `the directive name ${name} reads back to no node; ${escape}` }
|
||||
export function unknownDirectiveFault(name: string): ConvertFault {
|
||||
return { code: 'unknown-directive-name', message: `the directive name ${name} reads back to no node; ${directiveEscape}` }
|
||||
}
|
||||
|
||||
export function unsupportedNodeShape(message: string): ConvertFault {
|
||||
return { code: 'unsupported-node-shape', message }
|
||||
}
|
||||
|
||||
function attributePairFault(escape: string): ConvertFault {
|
||||
return malformedDirective(`${pairFault}; ${escape}`)
|
||||
}
|
||||
|
||||
function keyOrder(left: string, right: string): number {
|
||||
if (left < right) return -1
|
||||
return left > right ? 1 : 0
|
||||
@@ -168,28 +173,24 @@ function inlineDirectiveName(text: string, index: number): string | undefined {
|
||||
return opened === undefined ? undefined : opened.slice(directivePrefix.length, -1)
|
||||
}
|
||||
|
||||
function readDirectiveHeader(rest: string): Read<{ argument: string | undefined; attributes: DirectiveAttributes; name: string }> {
|
||||
directiveName.lastIndex = 0
|
||||
const name = directiveName.exec(rest)?.[0]
|
||||
if (name === undefined) return { fault: malformedDirective(nameFault) }
|
||||
let cursor = name.length
|
||||
function readOpenerRest(rest: string): Read<{ argument: string | undefined; attributes: DirectiveAttributes }> {
|
||||
let cursor = 0
|
||||
let argument: string | undefined
|
||||
let attributes = noAttributes
|
||||
if (rest.charAt(cursor) === ' ' && rest.charAt(cursor + 1) !== '{' && !lineEnd.test(rest.slice(cursor))) {
|
||||
bareRun.lastIndex = cursor + 1
|
||||
if (rest.charAt(0) === ' ' && rest.charAt(1) !== '{' && !lineEnd.test(rest)) {
|
||||
bareRun.lastIndex = 1
|
||||
argument = bareRun.exec(rest)?.[0]
|
||||
if (argument === undefined) return { fault: malformedDirective(shapeFault) }
|
||||
cursor += 1 + argument.length
|
||||
if (argument === undefined) return { fault: malformedDirective(openerFault) }
|
||||
cursor = 1 + argument.length
|
||||
}
|
||||
if (rest.charAt(cursor) === ' ' && rest.charAt(cursor + 1) === '{') {
|
||||
const read = readAttributes(rest, cursor + 1, directiveLineEscape)
|
||||
const read = readAttributesAt(rest, cursor + 1, false)
|
||||
if (read.fault !== undefined) return { fault: read.fault }
|
||||
if (read.value.attributes.size === 0) return { fault: malformedDirective(emptyFault) }
|
||||
attributes = read.value.attributes
|
||||
cursor += 1 + read.value.length
|
||||
cursor = read.value.end
|
||||
}
|
||||
if (!lineEnd.test(rest.slice(cursor))) return { fault: malformedDirective(shapeFault) }
|
||||
return { value: { argument, attributes, name } }
|
||||
if (!lineEnd.test(rest.slice(cursor))) return { fault: malformedDirective(openerFault) }
|
||||
return { value: { argument, attributes } }
|
||||
}
|
||||
|
||||
function readNestedDirective(text: string, index: number, depth: number): Read<DirectiveSpan> | undefined {
|
||||
@@ -199,23 +200,25 @@ function readNestedDirective(text: string, index: number, depth: number): Read<D
|
||||
if (depth > largestNesting) {
|
||||
return { fault: { code: 'unsupported-nesting-depth', message: `the input nests inline directives deeper than the ${largestNesting} levels the parser carries` } }
|
||||
}
|
||||
let cursor = index + directivePrefix.length + name.length
|
||||
let content: string | undefined
|
||||
if (text.charAt(cursor) === '[') {
|
||||
const end = readDirectiveContent(text, cursor + 1, depth)
|
||||
if (end.fault !== undefined) return { fault: end.fault }
|
||||
content = text.slice(cursor + 1, end.value)
|
||||
cursor = end.value + 1
|
||||
}
|
||||
let attributes = noAttributes
|
||||
if (text.charAt(cursor) === '{') {
|
||||
const read = readAttributes(text, cursor, inlineDirectiveEscape)
|
||||
if (read.fault !== undefined) return { fault: read.fault }
|
||||
if (read.value.attributes.size === 0 && content !== undefined) return { fault: malformedDirective(emptyFault) }
|
||||
attributes = read.value.attributes
|
||||
cursor += read.value.length
|
||||
}
|
||||
return { value: { attributes, content, length: cursor - index, name } }
|
||||
const slot = readContentSlot(text, index + directivePrefix.length + name.length, depth)
|
||||
if (slot.fault !== undefined) return { fault: slot.fault }
|
||||
const attributes = readAttributesAt(text, slot.value.end, slot.value.content === undefined)
|
||||
if (attributes.fault !== undefined) return { fault: attributes.fault }
|
||||
return { value: { attributes: attributes.value.attributes, content: slot.value.content, length: attributes.value.end - index, name } }
|
||||
}
|
||||
|
||||
function readContentSlot(text: string, index: number, depth: number): Read<Content> {
|
||||
if (text.charAt(index) !== '[') return { value: { content: undefined, end: index } }
|
||||
const close = readDirectiveContent(text, index + 1, depth)
|
||||
if (close.fault !== undefined) return { fault: close.fault }
|
||||
return { value: { content: text.slice(index + 1, close.value), end: close.value + 1 } }
|
||||
}
|
||||
|
||||
function readAttributesAt(text: string, index: number, braceClaims: boolean): Read<Attributes> {
|
||||
if (text.charAt(index) !== '{') return { value: { attributes: noAttributes, end: index } }
|
||||
const read = readAttributes(text, index)
|
||||
if (read.fault !== undefined || braceClaims || read.value.attributes.size > 0) return read
|
||||
return { fault: malformedDirective(emptyFault) }
|
||||
}
|
||||
|
||||
// A code span, an escape and a nested directive each bind before the content's own closing bracket.
|
||||
@@ -234,7 +237,7 @@ function readDirectiveContent(text: string, start: number, depth: number): Read<
|
||||
cursor = span
|
||||
continue
|
||||
}
|
||||
const nested = character === '!' ? readNestedDirective(text, cursor, depth + 1) : undefined
|
||||
const nested = readNestedDirective(text, cursor, depth + 1)
|
||||
if (nested !== undefined) {
|
||||
if (nested.fault !== undefined) return { fault: nested.fault }
|
||||
cursor += nested.value.length
|
||||
@@ -245,7 +248,7 @@ function readDirectiveContent(text: string, start: number, depth: number): Read<
|
||||
if (character === ']') brackets -= 1
|
||||
cursor += 1
|
||||
}
|
||||
return { fault: malformedDirective(`an inline directive [content] is unclosed; ${inlineDirectiveEscape}`) }
|
||||
return { fault: malformedDirective(`an inline directive [content] is unclosed; ${directiveEscape}`) }
|
||||
}
|
||||
|
||||
// `undefined` where the span crosses the line ending an inline directive may not cross.
|
||||
@@ -256,16 +259,16 @@ function readCodeSpanEnd(text: string, index: number): number | undefined {
|
||||
return text.slice(index, closer + opener).includes('\n') ? undefined : closer + opener
|
||||
}
|
||||
|
||||
function readAttributes(text: string, index: number, escape: string): Read<Attributes> {
|
||||
function readAttributes(text: string, index: number): Read<Attributes> {
|
||||
const attributes = new Map<string, DirectiveValue>()
|
||||
let cursor = index + 1
|
||||
let previous = ''
|
||||
while (cursor < text.length && text.charAt(cursor) !== '}') {
|
||||
if (attributes.size > 0) {
|
||||
if (text.charAt(cursor) !== ' ') return { fault: attributePairFault(escape) }
|
||||
if (text.charAt(cursor) !== ' ') return { fault: malformedDirective(pairFault) }
|
||||
cursor += 1
|
||||
}
|
||||
const pair = readAttributePair(text, cursor, escape)
|
||||
const pair = readAttributePair(text, cursor)
|
||||
if (pair.fault !== undefined) return { fault: pair.fault }
|
||||
const key = pair.value.key
|
||||
if (attributes.has(key)) return { fault: malformedDirective(`the attribute key ${key} is spelled twice`) }
|
||||
@@ -275,13 +278,13 @@ function readAttributes(text: string, index: number, escape: string): Read<Attri
|
||||
cursor = pair.value.end
|
||||
}
|
||||
if (text.charAt(cursor) !== '}') return { fault: malformedDirective('the {attrs} closing brace is missing') }
|
||||
return { value: { attributes, length: cursor + 1 - index } }
|
||||
return { value: { attributes, end: cursor + 1 } }
|
||||
}
|
||||
|
||||
function readAttributePair(text: string, index: number, escape: string): Read<AttributePair> {
|
||||
function readAttributePair(text: string, index: number): Read<AttributePair> {
|
||||
bareRun.lastIndex = index
|
||||
const key = bareRun.exec(text)?.[0]
|
||||
if (key === undefined || text.charAt(index + key.length) !== '=') return { fault: attributePairFault(escape) }
|
||||
if (key === undefined || text.charAt(index + key.length) !== '=') return { fault: malformedDirective(pairFault) }
|
||||
const start = index + key.length + 1
|
||||
if (text.charAt(start) === '"') {
|
||||
const quoted = readQuotedValue(text, start)
|
||||
@@ -290,7 +293,7 @@ function readAttributePair(text: string, index: number, escape: string): Read<At
|
||||
}
|
||||
bareRun.lastIndex = start
|
||||
const bare = bareRun.exec(text)?.[0]
|
||||
if (bare === undefined) return { fault: attributePairFault(escape) }
|
||||
if (bare === undefined) return { fault: malformedDirective(pairFault) }
|
||||
return { value: { end: start + bare.length, key, value: { decoded: bare, spelling: bare } } }
|
||||
}
|
||||
|
||||
|
||||
@@ -57,29 +57,29 @@ test('carries a text node attribute no spelling holds', () => {
|
||||
})
|
||||
|
||||
test('spells a CommonMark block as a directive where its own spelling holds neither attribute nor mark', () => {
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { localId: 'a' }, type: 'paragraph' }))), '::paragraph {localId=a}\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { wrap: true }, type: 'codeBlock' }))), ':::codeBlock {wrap=true}\n```\n```\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { localId: 'a' }, type: 'paragraph' }))), '!adf:paragraph {localId=a}\n!adf:/paragraph\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { wrap: true }, type: 'codeBlock' }))), '!adf:codeBlock {wrap=true}\n```\n```\n!adf:/codeBlock\n')
|
||||
assert.equal(markdown(adfToMarkdown(document(paragraph({ attrs: { localId: 'a' }, type: 'hardBreak' }, { text: 'x', type: 'text' })))), '!adf:hardBreak{localId=a}x\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ marks: [{ type: 'border' }], type: 'blockquote' }))), ':::blockquote {marks="[{\\"type\\":\\"border\\"}]"}\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ type: 'listItem' }))), ':::listItem\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ marks: [{ type: 'border' }], type: 'blockquote' }))), '!adf:blockquote {marks="[{\\"type\\":\\"border\\"}]"}\n!adf:/blockquote\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ type: 'listItem' }))), '!adf:listItem\n!adf:/listItem\n')
|
||||
})
|
||||
|
||||
test('spells an ordered list from the order attribute its first marker is', () => {
|
||||
const items: AdfNode[] = [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }]
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: items, type: 'orderedList' }))), '::::orderedList\n:::listItem\nx\n:::\n::::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: items, type: 'orderedList' }))), '!adf:orderedList\n!adf:listItem\nx\n!adf:/listItem\n!adf:/orderedList\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { order: 1 }, content: items, type: 'orderedList' }))), '1. x\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { order: 2 }, content: items, type: 'orderedList' }))), '2. x\n')
|
||||
})
|
||||
|
||||
test('spells a code block language no info string holds as an attribute', () => {
|
||||
const language = (value: string): string => markdown(adfToMarkdown(document({ attrs: { language: value }, type: 'codeBlock' })))
|
||||
assert.equal(language(''), ':::codeBlock {language=""}\n```\n```\n:::\n')
|
||||
assert.equal(language('a`b'), ':::codeBlock {language="a\\u0060b"}\n```\n```\n:::\n')
|
||||
assert.equal(language(' sql'), ':::codeBlock {language=" sql"}\n```\n```\n:::\n')
|
||||
assert.equal(language('adf'), ':::codeBlock {language="\\u0026#97;df"}\n```\n```\n:::\n')
|
||||
assert.equal(language('foo\\+bar'), ':::codeBlock {language="foo\\\\+bar"}\n```\n```\n:::\n')
|
||||
assert.equal(language('a\u0000b'), ':::codeBlock {language="a\\u0000b"}\n```\n```\n:::\n')
|
||||
assert.equal(language('a\tb'), ':::codeBlock {language="a\\tb"}\n```\n```\n:::\n')
|
||||
assert.equal(language(''), '!adf:codeBlock {language=""}\n```\n```\n!adf:/codeBlock\n')
|
||||
assert.equal(language('a`b'), '!adf:codeBlock {language="a\\u0060b"}\n```\n```\n!adf:/codeBlock\n')
|
||||
assert.equal(language(' sql'), '!adf:codeBlock {language=" sql"}\n```\n```\n!adf:/codeBlock\n')
|
||||
assert.equal(language('adf'), '!adf:codeBlock {language="\\u0026#97;df"}\n```\n```\n!adf:/codeBlock\n')
|
||||
assert.equal(language('foo\\+bar'), '!adf:codeBlock {language="foo\\\\+bar"}\n```\n```\n!adf:/codeBlock\n')
|
||||
assert.equal(language('a\u0000b'), '!adf:codeBlock {language="a\\u0000b"}\n```\n```\n!adf:/codeBlock\n')
|
||||
assert.equal(language('a\tb'), '!adf:codeBlock {language="a\\tb"}\n```\n```\n!adf:/codeBlock\n')
|
||||
})
|
||||
|
||||
test('refuses a link destination CommonMark cannot spell', () => {
|
||||
@@ -150,28 +150,29 @@ test('escapes the delimiter row a hard break leaves opening a pipe table with no
|
||||
|
||||
test('parts two adjacent lists of the same kind, the marker spelling being what merges', () => {
|
||||
const list: AdfNode = { content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }], type: 'bulletList' }
|
||||
assert.equal(markdown(adfToMarkdown(document(list, list))), '- x\n\n::listBreak\n\n- x\n')
|
||||
assert.equal(markdown(adfToMarkdown(document(list, list))), '- x\n\n!adf:listBreak\n\n- x\n')
|
||||
const ordered: AdfNode = { attrs: { order: 1 }, content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }], type: 'orderedList' }
|
||||
assert.equal(markdown(adfToMarkdown(document(ordered, ordered))), '1. x\n\n::listBreak\n\n1. x\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { panelType: 'info' }, content: [list, list], type: 'panel' }))), ':::panel info\n- x\n::listBreak\n- x\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document(ordered, ordered))), '1. x\n\n!adf:listBreak\n\n1. x\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { panelType: 'info' }, content: [list, list], type: 'panel' }))), '!adf:panel info\n- x\n!adf:listBreak\n- x\n!adf:/panel\n')
|
||||
const nested: AdfNode = { content: [{ content: [list, list], type: 'listItem' }], type: 'bulletList' }
|
||||
assert.equal(markdown(adfToMarkdown(document(nested))), '- - x\n\n ::listBreak\n\n - x\n')
|
||||
assert.equal(markdown(adfToMarkdown(document(nested))), '- - x\n\n !adf:listBreak\n\n - x\n')
|
||||
const carried: AdfNode = { ...list, attrs: { unknown: 'x' } }
|
||||
assert.ok(markdown(adfToMarkdown(document(carried, carried))).includes('```\n\n```adf\n'))
|
||||
assert.ok(markdown(adfToMarkdown(document(carried, carried))).includes('```\n\n```carry\n'))
|
||||
assert.ok(markdown(adfToMarkdown(document(carried, list))).endsWith('```\n\n- x\n'))
|
||||
assert.ok(markdown(adfToMarkdown(document(list, carried))).startsWith('- x\n\n```adf\n'))
|
||||
assert.ok(markdown(adfToMarkdown(document(list, carried))).startsWith('- x\n\n```carry\n'))
|
||||
})
|
||||
|
||||
test('carries a node type no section spells', () => {
|
||||
assert.equal(markdown(adfToMarkdown(document({ type: 'blockCard' }))), '```adf\n{\n "type": "blockCard"\n}\n```\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ type: 'toString' }))), '```adf\n{\n "type": "toString"\n}\n```\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ type: 'blockCard' }))), '```carry\n{\n "type": "blockCard"\n}\n```\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ type: 'toString' }))), '```carry\n{\n "type": "toString"\n}\n```\n')
|
||||
assert.equal(markdown(adfToMarkdown(document(paragraph({ type: 'blockCard' })))), '!adf:carry{json="{\\"type\\":\\"blockCard\\"}"}\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ text: 'x', type: 'text' }))), '```adf\n{\n "text": "x",\n "type": "text"\n}\n```\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ type: 'hardBreak' }))), '```adf\n{\n "type": "hardBreak"\n}\n```\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ text: 'x', type: 'text' }))), '```carry\n{\n "text": "x",\n "type": "text"\n}\n```\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ type: 'hardBreak' }))), '```carry\n{\n "type": "hardBreak"\n}\n```\n')
|
||||
})
|
||||
|
||||
test('spells the code block whose language is the reserved info string', () => {
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { language: 'adf' }, type: 'codeBlock' }))), ':::codeBlock {language=adf}\n```\n```\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { language: 'carry' }, type: 'codeBlock' }))), '!adf:codeBlock {language=carry}\n```\n```\n!adf:/codeBlock\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { language: 'adf' }, type: 'codeBlock' }))), '```adf\n```\n')
|
||||
})
|
||||
|
||||
test('breaks a mark run at the node it carries', () => {
|
||||
@@ -208,18 +209,18 @@ test('refuses a node whose content model the canonical form cannot emit', () =>
|
||||
})
|
||||
|
||||
test('spells a list its own content shape cannot hold as a directive', () => {
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: [paragraph()], type: 'bulletList' }))), ':::bulletList\n::paragraph\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ type: 'bulletList' }))), ':::bulletList\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { order: 2 }, content: [], type: 'orderedList' }))), ':::orderedList {order=2}\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: [paragraph()], type: 'bulletList' }))), '!adf:bulletList\n!adf:paragraph\n!adf:/paragraph\n!adf:/bulletList\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ type: 'bulletList' }))), '!adf:bulletList\n!adf:/bulletList\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { order: 2 }, content: [], type: 'orderedList' }))), '!adf:orderedList {order=2}\n!adf:/orderedList\n')
|
||||
})
|
||||
|
||||
test('spells an ordered list no marker fits as a directive', () => {
|
||||
const item: AdfNode = { content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }
|
||||
const list = (order: number, items: number): AdfDocument =>
|
||||
document({ attrs: { order }, content: Array.from({ length: items }, () => item), type: 'orderedList' })
|
||||
assert.equal(markdown(adfToMarkdown(list(1.5, 1))), '::::orderedList {order="1.5"}\n:::listItem\nx\n:::\n::::\n')
|
||||
assert.equal(markdown(adfToMarkdown(list(1.5, 1))), '!adf:orderedList {order="1.5"}\n!adf:listItem\nx\n!adf:/listItem\n!adf:/orderedList\n')
|
||||
assert.equal(markdown(adfToMarkdown(list(999999999, 1))), '999999999. x\n')
|
||||
assert.equal(markdown(adfToMarkdown(list(999999999, 2))), '::::orderedList {order=999999999}\n:::listItem\nx\n:::\n:::listItem\nx\n:::\n::::\n')
|
||||
assert.equal(markdown(adfToMarkdown(list(999999999, 2))), '!adf:orderedList {order=999999999}\n!adf:listItem\nx\n!adf:/listItem\n!adf:listItem\nx\n!adf:/listItem\n!adf:/orderedList\n')
|
||||
})
|
||||
|
||||
test('carries a code mark over anything but text', () => {
|
||||
@@ -230,8 +231,8 @@ test('carries a code mark over anything but text', () => {
|
||||
})
|
||||
|
||||
test('spells a heading level no ATX heading fits as a directive', () => {
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { level: 7 }, content: [{ text: 'x', type: 'text' }], type: 'heading' }))), ':::heading {level=7}\nx\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: [{ text: 'x', type: 'text' }], type: 'heading' }))), ':::heading\nx\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { level: 7 }, content: [{ text: 'x', type: 'text' }], type: 'heading' }))), '!adf:heading {level=7}\nx\n!adf:/heading\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: [{ text: 'x', type: 'text' }], type: 'heading' }))), '!adf:heading\nx\n!adf:/heading\n')
|
||||
})
|
||||
|
||||
test('escapes only text that would otherwise open a construct', () => {
|
||||
@@ -260,7 +261,9 @@ test('escapes only text that would otherwise open a construct', () => {
|
||||
assert.equal(emitted('!adf:mention[@x]{id=1}'), '\\!adf:mention[@x]{id=1}\n')
|
||||
assert.equal(emitted('!adf: completes nothing'), '\\!adf: completes nothing\n')
|
||||
assert.equal(emitted('!adfx and a! alone'), '!adfx and a! alone\n')
|
||||
assert.equal(emitted(':::panel info'), '\\:::panel info\n')
|
||||
assert.equal(emitted('!adf:/panel'), '\\!adf:/panel\n')
|
||||
assert.equal(emitted('!adf:rule'), '\\!adf:rule\n')
|
||||
assert.equal(emitted(':::panel info'), ':::panel info\n')
|
||||
assert.equal(emitted('10:30 tomorrow'), '10:30 tomorrow\n')
|
||||
assert.equal(emitted('[a](b)'), '\\[a](b)\n')
|
||||
assert.equal(emitted('**bold**'), '\\*\\*bold\\*\\*\n')
|
||||
@@ -330,8 +333,8 @@ test('parts a nested list the tight spelling would swallow from the block above
|
||||
assert.equal(markdown(adfToMarkdown(outer(text('a'), { content: [item(text('b'))], type: 'bulletList' }))), '- a\n - b\n')
|
||||
const list: AdfNode = { content: [item(text('b'))], type: 'bulletList' }
|
||||
const panel: AdfNode = { attrs: { panelType: 'info' }, content: [text('p')], type: 'panel' }
|
||||
assert.equal(markdown(adfToMarkdown(outer(panel, list))), '- :::panel info\n p\n :::\n\n - b\n')
|
||||
assert.ok(markdown(adfToMarkdown(outer(text('a'), { ...list, attrs: { unknown: 'x' } }))).startsWith('- a\n\n ```adf\n'))
|
||||
assert.equal(markdown(adfToMarkdown(outer(panel, list))), '- !adf:panel info\n p\n !adf:/panel\n\n - b\n')
|
||||
assert.ok(markdown(adfToMarkdown(outer(text('a'), { ...list, attrs: { unknown: 'x' } }))).startsWith('- a\n\n ```carry\n'))
|
||||
})
|
||||
|
||||
test('refuses marks and attributes nested deeper than the emitter carries', () => {
|
||||
@@ -422,10 +425,10 @@ test('escapes a hyphen underline a hard break would expose', () => {
|
||||
|
||||
test('spells a list item whose marker completes a thematic break as a directive', () => {
|
||||
const item = (...content: AdfNode[]): AdfNode => ({ content, type: 'listItem' })
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: [item({ type: 'rule' })], type: 'bulletList' }))), '::::bulletList\n:::listItem\n---\n:::\n::::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: [item({ type: 'rule' })], type: 'bulletList' }))), '!adf:bulletList\n!adf:listItem\n---\n!adf:/listItem\n!adf:/bulletList\n')
|
||||
const nested: AdfNode = { content: [item({ content: [item()], type: 'bulletList' })], type: 'bulletList' }
|
||||
assert.equal(markdown(adfToMarkdown(document(nested))), '- -\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: [item(nested)], type: 'bulletList' }))), '::::bulletList\n:::listItem\n- -\n:::\n::::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: [item(nested)], type: 'bulletList' }))), '!adf:bulletList\n!adf:listItem\n- -\n!adf:/listItem\n!adf:/bulletList\n')
|
||||
})
|
||||
|
||||
test('refuses the characters CommonMark rewrites', () => {
|
||||
@@ -494,29 +497,29 @@ test('emits an empty list item without trailing whitespace', () => {
|
||||
|
||||
test('spells a block directive as its node type, arg and attributes', () => {
|
||||
const panel = (attrs: AdfAttributes): AdfDocument => document({ attrs, content: [paragraph({ text: 'x', type: 'text' })], type: 'panel' })
|
||||
assert.equal(markdown(adfToMarkdown(panel({ panelType: 'warning' }))), ':::panel warning\nx\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(panel({}))), ':::panel\nx\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: [{ text: 'x', type: 'text' }], type: 'caption' }))), ':::caption\nx\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ type: 'caption' }))), '::caption\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { localId: 'a' }, type: 'syncBlock' }))), '::syncBlock {localId=a}\n')
|
||||
assert.equal(markdown(adfToMarkdown(panel({ panelType: 'warning' }))), '!adf:panel warning\nx\n!adf:/panel\n')
|
||||
assert.equal(markdown(adfToMarkdown(panel({}))), '!adf:panel\nx\n!adf:/panel\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: [{ text: 'x', type: 'text' }], type: 'caption' }))), '!adf:caption\nx\n!adf:/caption\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ type: 'caption' }))), '!adf:caption\n!adf:/caption\n')
|
||||
assert.equal(markdown(adfToMarkdown(document({ attrs: { localId: 'a' }, type: 'syncBlock' }))), '!adf:syncBlock {localId=a}\n')
|
||||
})
|
||||
|
||||
test('carries a directive attribute no section spells', () => {
|
||||
const carried = (node: AdfNode): string => markdown(adfToMarkdown(document(node)))
|
||||
assert.equal(carried({ attrs: { rounded: true }, type: 'panel' }), '```adf\n{\n "attrs": {\n "rounded": true\n },\n "type": "panel"\n}\n```\n')
|
||||
assert.equal(carried({ attrs: { toString: 'x' }, type: 'panel' }), '```adf\n{\n "attrs": {\n "toString": "x"\n },\n "type": "panel"\n}\n```\n')
|
||||
assert.equal(carried({ attrs: { localId: 4 }, type: 'panel' }), '```adf\n{\n "attrs": {\n "localId": 4\n },\n "type": "panel"\n}\n```\n')
|
||||
assert.equal(carried({ attrs: { rounded: true }, type: 'panel' }), '```carry\n{\n "attrs": {\n "rounded": true\n },\n "type": "panel"\n}\n```\n')
|
||||
assert.equal(carried({ attrs: { toString: 'x' }, type: 'panel' }), '```carry\n{\n "attrs": {\n "toString": "x"\n },\n "type": "panel"\n}\n```\n')
|
||||
assert.equal(carried({ attrs: { localId: 4 }, type: 'panel' }), '```carry\n{\n "attrs": {\n "localId": 4\n },\n "type": "panel"\n}\n```\n')
|
||||
assert.equal(
|
||||
carried({ attrs: { width: '50' }, type: 'layoutColumn' }),
|
||||
'```adf\n{\n "attrs": {\n "width": "50"\n },\n "type": "layoutColumn"\n}\n```\n',
|
||||
'```carry\n{\n "attrs": {\n "width": "50"\n },\n "type": "layoutColumn"\n}\n```\n',
|
||||
)
|
||||
assert.equal(
|
||||
carried({ attrs: { isNumberColumnEnabled: 'true' }, type: 'table' }),
|
||||
'```adf\n{\n "attrs": {\n "isNumberColumnEnabled": "true"\n },\n "type": "table"\n}\n```\n',
|
||||
'```carry\n{\n "attrs": {\n "isNumberColumnEnabled": "true"\n },\n "type": "table"\n}\n```\n',
|
||||
)
|
||||
assert.equal(
|
||||
carried({ content: [{ attrs: { alt: 4 }, type: 'media' }], type: 'mediaGroup' }),
|
||||
':::mediaGroup\n```adf\n{\n "attrs": {\n "alt": 4\n },\n "type": "media"\n}\n```\n:::\n',
|
||||
'!adf:mediaGroup\n```carry\n{\n "attrs": {\n "alt": 4\n },\n "type": "media"\n}\n```\n!adf:/mediaGroup\n',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -524,15 +527,15 @@ test('carries an arg slot value no bare token spells', () => {
|
||||
const carried = (node: AdfNode): string => markdown(adfToMarkdown(document(node)))
|
||||
assert.equal(
|
||||
carried({ attrs: { panelType: 'extra info' }, type: 'panel' }),
|
||||
'```adf\n{\n "attrs": {\n "panelType": "extra info"\n },\n "type": "panel"\n}\n```\n',
|
||||
'```carry\n{\n "attrs": {\n "panelType": "extra info"\n },\n "type": "panel"\n}\n```\n',
|
||||
)
|
||||
assert.equal(carried({ attrs: { state: 2 }, type: 'taskItem' }), '```adf\n{\n "attrs": {\n "state": 2\n },\n "type": "taskItem"\n}\n```\n')
|
||||
assert.equal(carried({ attrs: { state: 2 }, type: 'taskItem' }), '```carry\n{\n "attrs": {\n "state": 2\n },\n "type": "taskItem"\n}\n```\n')
|
||||
})
|
||||
|
||||
test('carries a block node mark in the reserved attribute', () => {
|
||||
const section = (...marks: AdfMark[]): AdfDocument => document({ marks, type: 'layoutSection' })
|
||||
assert.equal(markdown(adfToMarkdown(section({ type: 'breakout' }))), ':::layoutSection {marks="[{\\"type\\":\\"breakout\\"}]"}\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(section({ attrs: {}, type: 'breakout' }))), ':::layoutSection {marks="[{\\"type\\":\\"breakout\\"}]"}\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(section({ type: 'breakout' }))), '!adf:layoutSection {marks="[{\\"type\\":\\"breakout\\"}]"}\n!adf:/layoutSection\n')
|
||||
assert.equal(markdown(adfToMarkdown(section({ attrs: {}, type: 'breakout' }))), '!adf:layoutSection {marks="[{\\"type\\":\\"breakout\\"}]"}\n!adf:/layoutSection\n')
|
||||
})
|
||||
|
||||
test('refuses the content a directive body has no room for', () => {
|
||||
@@ -540,15 +543,15 @@ test('refuses the content a directive body has no room for', () => {
|
||||
assert.equal(markdown(adfToMarkdown(document({ text: 'x', type: 'panel' }))), 'unsupported-node-shape: a panel carries no text: this one holds text')
|
||||
})
|
||||
|
||||
test('separates blocks in a container body by a blank line only where the fence is not separation already', () => {
|
||||
test('separates blocks in a container body by a blank line only where a directive line is not separation already', () => {
|
||||
const text = (value: string): AdfNode => ({ content: [{ text: value, type: 'text' }], type: 'paragraph' })
|
||||
const panel = (...content: AdfNode[]): AdfDocument => document({ attrs: { panelType: 'info' }, content, type: 'panel' })
|
||||
assert.equal(markdown(adfToMarkdown(panel(text('a'), text('b')))), ':::panel info\na\n\nb\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(panel(text('a'), text('b')))), '!adf:panel info\na\n\nb\n!adf:/panel\n')
|
||||
const caption: AdfNode = { content: [{ text: 'c', type: 'text' }], type: 'caption' }
|
||||
assert.equal(markdown(adfToMarkdown(panel(caption, caption))), '::::panel info\n:::caption\nc\n:::\n:::caption\nc\n:::\n::::\n')
|
||||
assert.equal(markdown(adfToMarkdown(panel(text('a'), caption))), '::::panel info\na\n:::caption\nc\n:::\n::::\n')
|
||||
assert.equal(markdown(adfToMarkdown(panel(caption, text('a')))), '::::panel info\n:::caption\nc\n:::\na\n::::\n')
|
||||
assert.equal(markdown(adfToMarkdown(panel(paragraph(), text('a')))), ':::panel info\n::paragraph\na\n:::\n')
|
||||
assert.equal(markdown(adfToMarkdown(panel(caption, caption))), '!adf:panel info\n!adf:caption\nc\n!adf:/caption\n!adf:caption\nc\n!adf:/caption\n!adf:/panel\n')
|
||||
assert.equal(markdown(adfToMarkdown(panel(text('a'), caption))), '!adf:panel info\na\n!adf:caption\nc\n!adf:/caption\n!adf:/panel\n')
|
||||
assert.equal(markdown(adfToMarkdown(panel(caption, text('a')))), '!adf:panel info\n!adf:caption\nc\n!adf:/caption\na\n!adf:/panel\n')
|
||||
assert.equal(markdown(adfToMarkdown(panel(paragraph(), text('a')))), '!adf:panel info\n!adf:paragraph\n!adf:/paragraph\na\n!adf:/panel\n')
|
||||
})
|
||||
|
||||
test('spells the image form for exactly the centered external media shape', () => {
|
||||
@@ -558,7 +561,7 @@ test('spells the image form for exactly the centered external media shape', () =
|
||||
assert.equal(markdown(adfToMarkdown(single({ alt: 'The moon', type: 'external', url }))), `\n`)
|
||||
assert.equal(markdown(adfToMarkdown(single({ type: 'external', url }))), `\n`)
|
||||
assert.equal(markdown(adfToMarkdown(single({ alt: 'a [b] c', type: 'external', url }))), `![a \\[b\\] c](${url})\n`)
|
||||
const fallback = (attrs: AdfAttributes): boolean => markdown(adfToMarkdown(single(attrs))).startsWith(':::mediaSingle {layout=center}')
|
||||
const fallback = (attrs: AdfAttributes): boolean => markdown(adfToMarkdown(single(attrs))).startsWith('!adf:mediaSingle {layout=center}')
|
||||
assert.ok(fallback({ alt: '', type: 'external', url }))
|
||||
assert.ok(fallback({ alt: 'a\nb', type: 'external', url }))
|
||||
assert.ok(fallback({ alt: 'a\u0000b', type: 'external', url }))
|
||||
@@ -577,7 +580,7 @@ test('spells a mediaSingle the image form does not fit as a directive', () => {
|
||||
const single: AdfNode = { attrs: { layout: 'center' }, content: [media], marks: [{ type: 'border' }], type: 'mediaSingle' }
|
||||
assert.equal(
|
||||
markdown(adfToMarkdown(document(single))),
|
||||
':::mediaSingle {layout=center marks="[{\\"type\\":\\"border\\"}]"}\n::media {type=external url="https://example.com/moon.png"}\n:::\n',
|
||||
'!adf:mediaSingle {layout=center marks="[{\\"type\\":\\"border\\"}]"}\n!adf:media {type=external url="https://example.com/moon.png"}\n!adf:/mediaSingle\n',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -586,7 +589,7 @@ test('spells a table as a pipe table only where every row and cell is plain', ()
|
||||
const cell = (type: string, ...content: AdfNode[]): AdfNode => ({ content, type })
|
||||
const row = (...cells: AdfNode[]): AdfNode => ({ content: cells, type: 'tableRow' })
|
||||
const table = (...rows: AdfNode[]): AdfDocument => document({ content: rows, type: 'table' })
|
||||
const directive = (result: Result<string>): boolean => markdown(result).startsWith(':')
|
||||
const directive = (result: Result<string>): boolean => markdown(result).startsWith('!adf:table')
|
||||
const header = row(cell('tableHeader', text('Part')))
|
||||
assert.equal(markdown(adfToMarkdown(table(header, row(cell('tableCell', text('Bolt M8')))))), '| Part |\n| --- |\n| Bolt M8 |\n')
|
||||
assert.equal(markdown(adfToMarkdown(table(row(cell('tableHeader', text('a|b')))))), '| a\\|b |\n| --- |\n')
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
||||
import { adfDocumentFault, carriesOnly, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { blockDirective } from '../../adf/block-directives.ts'
|
||||
import { blockDirectiveForm } from '../block-directive-forms.ts'
|
||||
import { carriedBlock } from '../opaque-carry.ts'
|
||||
import { emitInlineLine } from './inline-line.ts'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
@@ -10,14 +11,14 @@ import { holdsNullCharacter, isBlankLine, isThematicBreak, markerInterruptsParag
|
||||
import { languageSlot } from '../code-language.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { listBreakSpelling } from '../list-break.ts'
|
||||
import { spellDirectiveHeader } from './block-directive-spelling.ts'
|
||||
import { spellBlockDirectiveOpener } from './block-directive-spelling.ts'
|
||||
import { spellDirectiveCloser } from '../directive-syntax.ts'
|
||||
import { tryImage } from './image.ts'
|
||||
import { tryPipeTable } from './pipe-table.ts'
|
||||
|
||||
type BlockContainer = 'directive' | 'document' | 'list-item'
|
||||
type BlockSpelling = 'commonmark' | 'directive' | 'list'
|
||||
type EmittedBody = { fenceColons: number; text: string }
|
||||
type EmittedBlock = EmittedBody & { spelling: BlockSpelling }
|
||||
type EmittedBlock = { spelling: BlockSpelling; text: string }
|
||||
type PlacedBlock = EmittedBlock & { node: AdfNode }
|
||||
|
||||
const largestListMarker = 999999999
|
||||
@@ -28,10 +29,10 @@ export function adfToMarkdown(document: AdfDocument): Result<string> {
|
||||
if (document.version !== 1) return failure('unsupported-document-version', `no markdown spelling carries ADF version ${document.version}`, [])
|
||||
const blocks = emitBlocks(nodeContent(document), 'document', [], 0)
|
||||
if (!blocks.ok) return blocks
|
||||
return success(blocks.value.text === '' ? '' : `${blocks.value.text}\n`)
|
||||
return success(blocks.value === '' ? '' : `${blocks.value}\n`)
|
||||
}
|
||||
|
||||
function emitBlocks(nodes: readonly AdfNode[], container: BlockContainer, path: ConvertErrorPath, depth: number): Result<EmittedBody> {
|
||||
function emitBlocks(nodes: readonly AdfNode[], container: BlockContainer, path: ConvertErrorPath, depth: number): Result<string> {
|
||||
if (depth > largestNesting) return failure('unsupported-nesting-depth', `the document nests deeper than the ${largestNesting} levels the emitter carries`, path)
|
||||
const blocks: PlacedBlock[] = []
|
||||
for (const [index, node] of nodes.entries()) {
|
||||
@@ -39,15 +40,13 @@ function emitBlocks(nodes: readonly AdfNode[], container: BlockContainer, path:
|
||||
if (!block.ok) return block
|
||||
blocks.push({ ...block.value, node })
|
||||
}
|
||||
let fenceColons = 0
|
||||
let text = ''
|
||||
for (const [index, block] of blocks.entries()) {
|
||||
const previous = blocks[index - 1]
|
||||
if (previous !== undefined) text += separationBetween(previous, block, container)
|
||||
fenceColons = Math.max(fenceColons, block.fenceColons)
|
||||
text += block.text
|
||||
}
|
||||
return success({ fenceColons, text })
|
||||
return success(text)
|
||||
}
|
||||
|
||||
function separationBetween(previous: PlacedBlock, next: PlacedBlock, container: BlockContainer): string {
|
||||
@@ -105,42 +104,36 @@ function commonMarkLine(text: Result<string>): Result<EmittedBlock> {
|
||||
}
|
||||
|
||||
function commonMarkText(text: string): EmittedBlock {
|
||||
return { fenceColons: 0, spelling: 'commonmark', text }
|
||||
return { spelling: 'commonmark', text }
|
||||
}
|
||||
|
||||
function directivePair(node: AdfNode, opener: string, body: string): EmittedBlock {
|
||||
return { spelling: 'directive', text: `${opener}\n${body === '' ? '' : `${body}\n`}${spellDirectiveCloser(node.type)}` }
|
||||
}
|
||||
|
||||
function emitDirectiveBlock(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
||||
if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text: this one holds text`, path)
|
||||
const content = nodeContent(node)
|
||||
if (directive.contentModel === 'none' && content.length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content: this one holds some`, path)
|
||||
const leaf = blockDirectiveForm(node.type) === 'leaf'
|
||||
if (leaf && content.length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content: this one holds some`, path)
|
||||
if (directive.contentModel === 'code') return emitCodeDirective(node, directive, path, depth)
|
||||
const header = spellDirectiveHeader(node, directive)
|
||||
if (header === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
||||
if (directive.contentModel === 'none' || (directive.contentModel === 'inline' && content.length === 0)) {
|
||||
return success({ fenceColons: 2, spelling: 'directive', text: `::${header}` })
|
||||
}
|
||||
const body = directive.contentModel === 'inline' ? emitInlineBody(content, path) : emitBlocks(content, 'directive', path, depth + 1)
|
||||
const opener = spellBlockDirectiveOpener(node, directive)
|
||||
if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
||||
if (leaf) return success({ spelling: 'directive', text: opener })
|
||||
const body = directive.contentModel === 'inline' ? emitInlineLine(content, 'paragraph', path) : emitBlocks(content, 'directive', path, depth + 1)
|
||||
if (!body.ok) return body
|
||||
const fenceColons = Math.max(3, body.value.fenceColons + 1)
|
||||
const fence = ':'.repeat(fenceColons)
|
||||
const lines = body.value.text === '' ? '' : `${body.value.text}\n`
|
||||
return success({ fenceColons, spelling: 'directive', text: `${fence}${header}\n${lines}${fence}` })
|
||||
}
|
||||
|
||||
function emitInlineBody(content: readonly AdfNode[], path: ConvertErrorPath): Result<EmittedBody> {
|
||||
const line = emitInlineLine(content, 'paragraph', path)
|
||||
if (!line.ok) return line
|
||||
return success({ fenceColons: 0, text: line.value })
|
||||
return success(directivePair(node, opener, body.value))
|
||||
}
|
||||
|
||||
function emitBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number): Result<EmittedBlock> | undefined {
|
||||
if (!carriesOnly(node, [])) return undefined
|
||||
const inner = emitBlocks(nodeContent(node), 'document', path, depth + 1)
|
||||
if (!inner.ok) return inner
|
||||
const text = inner.value.text
|
||||
const text = inner.value
|
||||
.split('\n')
|
||||
.map((line) => (line === '' ? '>' : `> ${line}`))
|
||||
.join('\n')
|
||||
return success({ fenceColons: inner.value.fenceColons, spelling: 'commonmark', text })
|
||||
return success(commonMarkText(text))
|
||||
}
|
||||
|
||||
function emitCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||
@@ -154,12 +147,11 @@ function emitCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlo
|
||||
|
||||
function emitCodeDirective(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
||||
const slot = languageSlot(nodeAttrs(node)['language'])
|
||||
const header = spellDirectiveHeader(node, directive, slot.kind === 'attribute' ? [] : ['language'])
|
||||
if (header === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
||||
const opener = spellBlockDirectiveOpener(node, directive, slot.kind === 'attribute' ? [] : ['language'])
|
||||
if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
||||
const text = codeBlockText(node, path)
|
||||
if (!text.ok) return text
|
||||
const info = slot.kind === 'fence' ? slot.info : ''
|
||||
return success({ fenceColons: 3, spelling: 'directive', text: `:::${header}\n${fencedCodeBlock(info, text.value)}\n:::` })
|
||||
return success(directivePair(node, opener, fencedCodeBlock(slot.kind === 'fence' ? slot.info : '', text.value)))
|
||||
}
|
||||
|
||||
function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result<string> {
|
||||
@@ -203,15 +195,13 @@ function emitList(node: AdfNode, path: ConvertErrorPath, depth: number): Result<
|
||||
if (start === undefined || items.length === 0) return undefined
|
||||
if (items.some((item) => item.type !== 'listItem' || !carriesOnly(item, []))) return undefined
|
||||
const lines: string[] = []
|
||||
let fenceColons = 0
|
||||
for (const [offset, item] of items.entries()) {
|
||||
const emitted = emitListItem(item, ordered ? `${start + offset}. ` : '- ', [...path, 'content', offset], depth)
|
||||
if (emitted === undefined) return undefined
|
||||
if (!emitted.ok) return emitted
|
||||
fenceColons = Math.max(fenceColons, emitted.value.fenceColons)
|
||||
lines.push(emitted.value.text)
|
||||
lines.push(emitted.value)
|
||||
}
|
||||
return success({ fenceColons, spelling: 'list', text: lines.join('\n') })
|
||||
return success({ spelling: 'list', text: lines.join('\n') })
|
||||
}
|
||||
|
||||
function listStart(node: AdfNode, items: number): number | undefined {
|
||||
@@ -221,16 +211,16 @@ function listStart(node: AdfNode, items: number): number | undefined {
|
||||
return start + items - 1 > largestListMarker ? undefined : start
|
||||
}
|
||||
|
||||
function emitListItem(item: AdfNode, marker: string, path: ConvertErrorPath, depth: number): Result<EmittedBody> | undefined {
|
||||
function emitListItem(item: AdfNode, marker: string, path: ConvertErrorPath, depth: number): Result<string> | undefined {
|
||||
const inner = emitBlocks(nodeContent(item), 'list-item', path, depth + 1)
|
||||
if (!inner.ok) return inner
|
||||
if (inner.value.text === '') return success({ fenceColons: 0, text: marker.trimEnd() })
|
||||
const body = inner.value.text.split('\n')
|
||||
if (inner.value === '') return success(marker.trimEnd())
|
||||
const body = inner.value.split('\n')
|
||||
if (body.some((line) => line !== '' && isBlankLine(line))) return undefined
|
||||
const indent = ' '.repeat(marker.length)
|
||||
const lines = body.map((line, index) => (index === 0 ? `${marker}${line}` : line === '' ? '' : `${indent}${line}`))
|
||||
if (isThematicBreak(lines[0] ?? '')) return undefined
|
||||
return success({ fenceColons: inner.value.fenceColons, text: lines.join('\n') })
|
||||
return success(lines.join('\n'))
|
||||
}
|
||||
|
||||
function emitParagraph(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { AdfNode } from '../../adf/document.ts'
|
||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
||||
import { blockArgument } from '../block-directive-arguments.ts'
|
||||
import { isBareToken, spellAttributes, spellJsonAttribute, spellVocabulary } from '../directive-syntax.ts'
|
||||
import { directivePrefix, isBareToken, spellAttributes, spellJsonAttribute, spellVocabulary } from '../directive-syntax.ts'
|
||||
import { markValues, marksAttribute } from '../block-directive-marks.ts'
|
||||
import { nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
||||
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
|
||||
|
||||
export function spellDirectiveHeader(node: AdfNode, directive: BlockDirective, spelledByBody: readonly string[] = []): string | undefined {
|
||||
export function spellBlockDirectiveOpener(node: AdfNode, directive: BlockDirective, spelledByBody: readonly string[] = []): string | undefined {
|
||||
const argumentAttribute = blockArgument(node.type)
|
||||
const argument = spellArgument(node, argumentAttribute)
|
||||
if (argument === undefined) return undefined
|
||||
@@ -17,7 +17,7 @@ export function spellDirectiveHeader(node: AdfNode, directive: BlockDirective, s
|
||||
const marks = nodeMarks(node)
|
||||
if (marks.length > 0) spelledPairs.push([marksAttribute, spellJsonAttribute(markValues(marks))])
|
||||
const attributes = spellAttributes(spelledPairs)
|
||||
return `${node.type}${argument}${attributes === '' ? '' : ` ${attributes}`}`
|
||||
return `${directivePrefix}${node.type}${argument}${attributes === '' ? '' : ` ${attributes}`}`
|
||||
}
|
||||
|
||||
function spellArgument(node: AdfNode, argumentAttribute: string | undefined): string | undefined {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { carriedInline } from '../opaque-carry.ts'
|
||||
import { claimsLine, holdsNullCharacter, isAutolink } from '../commonmark-grammar.ts'
|
||||
import { escapeUnbalanced, spellDestination, spellLinkTarget } from '../link-syntax.ts'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { holdsDirectivePrefix, slotLineEndingFault, spellInlineDirectiveOpener, spellLeafDirective } from '../directive-syntax.ts'
|
||||
import { holdsDirectivePrefix, slotLineEndingFault, spellInlineDirectiveOpener, spellInlineLeafDirective } from '../directive-syntax.ts'
|
||||
import { holdsEntityReference } from '../entity-references.ts'
|
||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
@@ -199,7 +199,7 @@ function emitHardBreak(node: AdfNode, directive: InlineDirective, context: Inlin
|
||||
const attributes = spellInlineNodeAttributes(node, directive)
|
||||
if (attributes === undefined) return success({ carry: { first: index, last: index } })
|
||||
if (attributes === '' && context.spansLines && !context.atBlockEnd) return success({ segments: [syntax('\\\n')] })
|
||||
return success({ segments: [syntax(spellLeafDirective('hardBreak', attributes))] })
|
||||
return success({ segments: [syntax(spellInlineLeafDirective('hardBreak', attributes))] })
|
||||
}
|
||||
|
||||
function emitInlineDirective(node: AdfNode, directive: InlineDirective, index: number, path: ConvertErrorPath): Result<Emission> {
|
||||
@@ -208,7 +208,7 @@ function emitInlineDirective(node: AdfNode, directive: InlineDirective, index: n
|
||||
const attributes = spellInlineNodeAttributes(node, directive)
|
||||
if (attributes === undefined) return success({ carry: { first: index, last: index } })
|
||||
const slot = directive.textAttribute === undefined ? undefined : nodeAttrs(node)[directive.textAttribute]
|
||||
if (slot === undefined) return success({ segments: [syntax(spellLeafDirective(node.type, attributes))] })
|
||||
if (slot === undefined) return success({ segments: [syntax(spellInlineLeafDirective(node.type, attributes))] })
|
||||
if (typeof slot !== 'string') return success({ carry: { first: index, last: index } })
|
||||
const spans = slotLineEndingFault(node.type, slot)
|
||||
if (spans !== undefined) return faulted(spans, path)
|
||||
|
||||
@@ -98,7 +98,7 @@ function escapedIndexes(scan: string, escapings: readonly InlineEscaping[], cont
|
||||
mergesWithSyntax(scan, escapings, index) ||
|
||||
opensConstruct(scan, linkClose, index, escaping === 'bracketed', container, escaped))) ||
|
||||
(escaping === 'bracketed-link-target' &&
|
||||
((scan.charAt(index) === '`' && opensCodeSpan(scan, index, escaped)) || (scan.charAt(index) === '!' && claimsDirectivePrefix(scan, index))))
|
||||
((scan.charAt(index) === '`' && opensCodeSpan(scan, index, escaped)) || claimsDirectivePrefix(scan, index)))
|
||||
) {
|
||||
escaped.add(index)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { directivePrefix } from './directive-syntax.ts'
|
||||
|
||||
export const listBreakName = 'listBreak'
|
||||
|
||||
export const listBreakSpelling = `::${listBreakName}`
|
||||
export const listBreakSpelling = `${directivePrefix}${listBreakName}`
|
||||
|
||||
@@ -6,10 +6,9 @@ import { isAdfNode } from '../adf/document.ts'
|
||||
import { isJsonValue, overNested } from '../json-value.ts'
|
||||
import { fencedCodeBlock } from './backtick-runs.ts'
|
||||
import { largestNesting } from '../nesting.ts'
|
||||
import { malformedDirective, readSoleStringAttribute, spellAttributes, spellLeafDirective, spellStringAttribute, unsupportedNodeShape } from './directive-syntax.ts'
|
||||
import { malformedDirective, readSoleStringAttribute, spellAttributes, spellInlineLeafDirective, spellStringAttribute, unsupportedNodeShape } from './directive-syntax.ts'
|
||||
import { serializeCanonicalJson } from '../canonical-json.ts'
|
||||
|
||||
export const carryFence = 'adf'
|
||||
export const carryName = 'carry'
|
||||
|
||||
const jsonAttribute = 'json'
|
||||
@@ -17,13 +16,13 @@ const jsonAttribute = 'json'
|
||||
export function carriedBlock(node: AdfNode, path: ConvertErrorPath, depth: number): Result<string> {
|
||||
const json = carriedJson(node, 'two-space', path, largestNesting - depth)
|
||||
if (!json.ok) return json
|
||||
return success(fencedCodeBlock(carryFence, json.value))
|
||||
return success(fencedCodeBlock(carryName, json.value))
|
||||
}
|
||||
|
||||
export function carriedInline(node: AdfNode, path: ConvertErrorPath): Result<string> {
|
||||
const json = carriedJson(node, 'compact', path, largestNesting)
|
||||
if (!json.ok) return json
|
||||
return success(spellLeafDirective(carryName, spellAttributes([[jsonAttribute, spellStringAttribute(json.value)]])))
|
||||
return success(spellInlineLeafDirective(carryName, spellAttributes([[jsonAttribute, spellStringAttribute(json.value)]])))
|
||||
}
|
||||
|
||||
export function readCarriedBlock(body: string, depth: number): Read<AdfNode> {
|
||||
|
||||
@@ -69,44 +69,51 @@ test('leaves the paragraph a line no definition spells', () => {
|
||||
|
||||
test('swallows an HTML block to the end condition its start sets', () => {
|
||||
assert.deepEqual(kinds('<div>\nx\n\nPart.\n'), ['html', 'paragraph'])
|
||||
assert.deepEqual(kinds('<!--\n:::\n-->\nPart.\n'), ['html', 'paragraph'])
|
||||
assert.deepEqual(kinds('<!--\n!adf:/panel\n-->\nPart.\n'), ['html', 'paragraph'])
|
||||
assert.deepEqual(kinds('<pre>x</pre>\nPart.\n'), ['html', 'paragraph'])
|
||||
assert.deepEqual(kinds('<div>\nx\n'), ['html'])
|
||||
assert.deepEqual(kinds('Part.\n<div>\n'), ['paragraph', 'html'])
|
||||
})
|
||||
|
||||
test('carries a claimed line as the block it opens, the refusal the node layer builds', () => {
|
||||
assert.deepEqual(kinds(':::\nPart.\n'), ['fault', 'paragraph'])
|
||||
assert.deepEqual(kinds('!adf:/panel\nPart.\n'), ['fault', 'paragraph'])
|
||||
assert.deepEqual(kinds('Part.\n| x |\n'), ['paragraph', 'fault'])
|
||||
})
|
||||
|
||||
test('holds a directive container open until the fence that closes it', () => {
|
||||
assert.deepEqual(kinds(':::panel info\nPart.\n:::\nMore.\n'), ['directive', 'paragraph'])
|
||||
assert.deepEqual(kinds('::rule\nPart.\n'), ['directive', 'paragraph'])
|
||||
assert.deepEqual(faults(':::panel info\n\nPart.\n\n:::\n'), [])
|
||||
assert.deepEqual(faults(':::panel info\n> Part.\n> :::\n'), [])
|
||||
assert.deepEqual(faults('::::panel info\n- :::expand\n Part.\n :::\n::::\n'), [])
|
||||
assert.deepEqual(faults(':::panel info\n```\n:::\n```\n:::\n'), [])
|
||||
assert.deepEqual(parseBlocks(':::panel info {panelColor="#ff0000"}\nPart.\n:::\n').blocks, [
|
||||
test('opens a container where the content model takes content, and holds it open until the closer naming it', () => {
|
||||
assert.deepEqual(kinds('!adf:panel info\nPart.\n!adf:/panel\nMore.\n'), ['directive', 'paragraph'])
|
||||
assert.deepEqual(kinds('!adf:rule\nPart.\n'), ['directive', 'paragraph'])
|
||||
assert.deepEqual(kinds('!adf:listBreak\nPart.\n'), ['directive', 'paragraph'])
|
||||
assert.deepEqual(kinds('!adf:widget\nPart.\n'), ['directive', 'paragraph'])
|
||||
assert.deepEqual(kinds('!adf:mention\nPart.\n'), ['directive', 'paragraph'])
|
||||
assert.deepEqual(faults('!adf:panel info\n\nPart.\n\n!adf:/panel\n'), [])
|
||||
assert.deepEqual(faults('!adf:panel info\n> Part.\n!adf:/panel\n'), [])
|
||||
assert.deepEqual(faults('!adf:panel info\n- !adf:panel warning\n Part.\n !adf:/panel\n!adf:/panel\n'), [])
|
||||
assert.deepEqual(faults('!adf:panel info\n```\n!adf:/panel\n```\n!adf:/panel\n'), [])
|
||||
assert.deepEqual(parseBlocks('!adf:panel info {panelColor="#ff0000"}\nPart.\n!adf:/panel\n').blocks, [
|
||||
{
|
||||
argument: 'info',
|
||||
attributes: new Map([['panelColor', { decoded: '#ff0000', spelling: '"#ff0000"' }]]),
|
||||
blocks: [{ kind: 'paragraph', position: { line: 2, offset: 37 }, text: 'Part.' }],
|
||||
blocks: [{ kind: 'paragraph', position: { line: 2, offset: 39 }, text: 'Part.' }],
|
||||
kind: 'directive',
|
||||
name: 'panel',
|
||||
position: { line: 1, offset: 0 },
|
||||
},
|
||||
])
|
||||
assert.deepEqual(parseBlocks('::rule\n').blocks, [
|
||||
assert.deepEqual(parseBlocks('!adf:rule\n').blocks, [
|
||||
{ argument: undefined, attributes: new Map(), blocks: undefined, kind: 'directive', name: 'rule', position: { line: 1, offset: 0 } },
|
||||
])
|
||||
})
|
||||
|
||||
test('names the directive fence a container does not sit longer than', () => {
|
||||
assert.deepEqual(faults(':::panel info\n:::expand\nPart.\n:::\n'), ["a directive fence line is at least as long as the container's 3 colons"])
|
||||
assert.deepEqual(faults('::::panel info\n:::\n::::\n'), ['a closing fence is shorter than the 4 colons it would close'])
|
||||
assert.deepEqual(faults('::::panel info\n:::expand\nPart.\n:::::\n::::\n'), [])
|
||||
assert.deepEqual(faults(':::panel info\nPart.\n'), ['a container fenced with 3 colons is unclosed'])
|
||||
assert.deepEqual(faults('- :::panel info\n\nPart.\n'), ['a container fenced with 3 colons is unclosed'])
|
||||
assert.deepEqual(faults('Part.\n\n:::\n'), ['a closing fence closes no open container; \\::: keeps the line literal text'])
|
||||
test('closes the containers a closer names past as unclosed, and crosses no list item or blockquote edge', () => {
|
||||
const unclosed = (name: string): string => `the ${name} container is unclosed: no !adf:/${name} follows inside the block holding it`
|
||||
const unopened = (name: string): string => `the closer !adf:/${name} closes no ${name} container open where it stands; \\!adf: keeps the prefix literal`
|
||||
assert.deepEqual(faults('!adf:panel info\n!adf:expand\nPart.\n!adf:/expand\n!adf:/panel\n'), [])
|
||||
assert.deepEqual(faults('!adf:panel info\n!adf:expand\n!adf:layoutSection\nPart.\n!adf:/panel\n'), [unclosed('expand')])
|
||||
assert.deepEqual(faults('!adf:panel info\nPart.\n'), [unclosed('panel')])
|
||||
assert.deepEqual(faults('- !adf:panel info\n\nPart.\n'), [unclosed('panel')])
|
||||
assert.deepEqual(faults('> !adf:panel info\n> Part.\n!adf:/panel\n'), [unclosed('panel'), unopened('panel')])
|
||||
assert.deepEqual(faults('!adf:panel info\n> !adf:/panel\n!adf:/panel\n'), [unopened('panel')])
|
||||
assert.deepEqual(faults('Part.\n\n!adf:/panel\n'), [unopened('panel')])
|
||||
assert.deepEqual(faults('!adf:rule {localId=a-1}\nPart.\n!adf:/rule\n'), ['rule takes no body, so no !adf:/rule closes it'])
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { DirectiveAttributes, DirectiveLine } from '../directive-syntax.ts'
|
||||
import type { LinkDefinition } from '../link-syntax.ts'
|
||||
import {
|
||||
atxHeading,
|
||||
claimsDirectiveLine,
|
||||
claimsPipeLine,
|
||||
closingCodeFence,
|
||||
decodeTextEscapes,
|
||||
@@ -16,7 +15,8 @@ import {
|
||||
replaceNullCharacters,
|
||||
setextHeadingLevel,
|
||||
} from '../commonmark-grammar.ts'
|
||||
import { directiveLineEscape, malformedDirective, readDirectiveLine } from '../directive-syntax.ts'
|
||||
import { blockDirectiveForm } from '../block-directive-forms.ts'
|
||||
import { directiveEscape, malformedDirective, readDirectiveLine, spellDirectiveCloser } from '../directive-syntax.ts'
|
||||
import { barePipeCells, isDelimiterRow, isPipeAlignment, isPipeDelimiter, malformedPipeTable, pipeCells } from '../pipe-table-syntax.ts'
|
||||
import { readLinkDefinitions } from '../link-reference-definitions.ts'
|
||||
|
||||
@@ -40,7 +40,7 @@ export type DirectiveBlock = Extract<Block, { kind: 'directive' }>
|
||||
|
||||
type ListBlock = Extract<Block, { items: Block[][] }>
|
||||
|
||||
type OpenDirective = { blocks: Block[]; colons: number; index: number; kind: 'directive'; parent: Block[]; position: SourcePosition }
|
||||
type OpenDirective = { blocks: Block[]; index: number; kind: 'directive'; name: string; parent: Block[]; position: SourcePosition }
|
||||
|
||||
type OpenContainer =
|
||||
| Extract<Block, { kind: 'blockquote' }>
|
||||
@@ -64,7 +64,6 @@ type Walk = ParsedBlocks & { leaf: OpenLeaf | undefined; position: SourcePositio
|
||||
|
||||
const indentedCodeColumns = 4
|
||||
const largestOpenerIndentation = 3
|
||||
const leafColons = 2
|
||||
const tabStop = 4
|
||||
|
||||
export function parseBlocks(markdown: string): ParsedBlocks {
|
||||
@@ -114,7 +113,7 @@ function matchContainers(walk: Walk, line: Line): { depth: number; rest: Line }
|
||||
|
||||
function continuesContainer(walk: Walk, container: OpenContainer, line: Line): Line | undefined {
|
||||
if (container.kind === 'blockquote') return blockquoteRest(removeColumns(line, largestOpenerIndentation))
|
||||
// A directive container has no continuation marker: only its own fence closes it.
|
||||
// A directive container has no continuation marker: only its own closer closes it.
|
||||
if (container.kind === 'directive') return line
|
||||
// A list item begins with at most one blank line: an empty one gives the second up.
|
||||
if (isBlankLine(line.text)) {
|
||||
@@ -202,7 +201,7 @@ function closeContainers(walk: Walk, depth: number): void {
|
||||
for (const container of walk.stack.slice(depth)) {
|
||||
if (container.kind !== 'directive') continue
|
||||
container.parent[container.index] = {
|
||||
fault: malformedDirective(`a container fenced with ${container.colons} colons is unclosed`),
|
||||
fault: malformedDirective(`the ${container.name} container is unclosed: no ${spellDirectiveCloser(container.name)} follows inside the block holding it`),
|
||||
kind: 'fault',
|
||||
position: container.position,
|
||||
}
|
||||
@@ -214,50 +213,48 @@ function dropContainers(walk: Walk, depth: number): void {
|
||||
walk.stack.length = depth
|
||||
}
|
||||
|
||||
function openDirective(walk: Walk, directive: Extract<DirectiveLine, { kind: 'header' }>): void {
|
||||
function applyDirectiveLine(walk: Walk, directive: DirectiveLine): void {
|
||||
if (directive.kind === 'closer') closeDirective(walk, directive.name)
|
||||
else openDirective(walk, directive)
|
||||
}
|
||||
|
||||
function openDirective(walk: Walk, directive: Extract<DirectiveLine, { kind: 'opener' }>): void {
|
||||
const { name } = directive
|
||||
const block: DirectiveBlock = {
|
||||
argument: directive.argument,
|
||||
attributes: directive.attributes,
|
||||
blocks: directive.colons > leafColons ? [] : undefined,
|
||||
blocks: blockDirectiveForm(name) === 'container' ? [] : undefined,
|
||||
kind: 'directive',
|
||||
name: directive.name,
|
||||
name,
|
||||
position: walk.position,
|
||||
}
|
||||
const parent = currentBlocks(walk)
|
||||
parent.push(block)
|
||||
const { position } = block
|
||||
if (block.blocks !== undefined) walk.stack.push({ blocks: block.blocks, colons: directive.colons, index: parent.length - 1, kind: 'directive', parent, position })
|
||||
if (block.blocks !== undefined) walk.stack.push({ blocks: block.blocks, index: parent.length - 1, kind: 'directive', name, parent, position })
|
||||
}
|
||||
|
||||
function applyDirectiveLine(walk: Walk, directive: DirectiveLine): void {
|
||||
const enclosing = innermostDirective(walk)
|
||||
if (directive.kind === 'closing') {
|
||||
closeDirective(walk, directive.colons, enclosing)
|
||||
function closeDirective(walk: Walk, name: string): void {
|
||||
const closer = spellDirectiveCloser(name)
|
||||
if (blockDirectiveForm(name) === 'leaf') {
|
||||
pushFault(walk, malformedDirective(`${name} takes no body, so no ${closer} closes it`))
|
||||
return
|
||||
}
|
||||
if (enclosing !== undefined && directive.colons >= enclosing.container.colons) {
|
||||
pushFault(walk, malformedDirective(`a directive fence line is at least as long as the container's ${enclosing.container.colons} colons`))
|
||||
const depth = openDirectiveDepth(walk, name)
|
||||
if (depth === undefined) {
|
||||
pushFault(walk, malformedDirective(`the closer ${closer} closes no ${name} container open where it stands; ${directiveEscape}`))
|
||||
return
|
||||
}
|
||||
openDirective(walk, directive)
|
||||
closeContainers(walk, depth + 1)
|
||||
dropContainers(walk, depth)
|
||||
}
|
||||
|
||||
function closeDirective(walk: Walk, colons: number, enclosing: { container: OpenDirective; depth: number } | undefined): void {
|
||||
if (enclosing === undefined) {
|
||||
pushFault(walk, malformedDirective(`a closing fence closes no open container; ${directiveLineEscape}`))
|
||||
return
|
||||
}
|
||||
if (colons < enclosing.container.colons) {
|
||||
pushFault(walk, malformedDirective(`a closing fence is shorter than the ${enclosing.container.colons} colons it would close`))
|
||||
return
|
||||
}
|
||||
dropContainers(walk, enclosing.depth)
|
||||
}
|
||||
|
||||
function innermostDirective(walk: Walk): { container: OpenDirective; depth: number } | undefined {
|
||||
// A closer crosses no list item or blockquote edge.
|
||||
function openDirectiveDepth(walk: Walk, name: string): number | undefined {
|
||||
for (let depth = walk.stack.length - 1; depth >= 0; depth -= 1) {
|
||||
const container = walk.stack[depth]
|
||||
if (container?.kind === 'directive') return { container, depth }
|
||||
if (container?.kind !== 'directive') return undefined
|
||||
if (container.name === name) return depth
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -271,7 +268,7 @@ function continuesLazily(walk: Walk, line: Line): boolean {
|
||||
if (walk.leaf?.kind !== 'paragraph' || isBlankLine(line.text)) return false
|
||||
if (leadingColumns(line) >= indentedCodeColumns) return true
|
||||
const opener = removeColumns(line, largestOpenerIndentation).text
|
||||
if (claimsDirectiveLine(opener) || claimsPipeLine(opener) || isThematicBreak(opener)) return false
|
||||
if (readDirectiveLine(opener) !== undefined || claimsPipeLine(opener) || isThematicBreak(opener)) return false
|
||||
return atxHeading(opener) === undefined && openingCodeFence(opener) === undefined && openingHtmlBlock(opener, true) === undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ import type { ConvertFault } from '../../result.ts'
|
||||
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
|
||||
import type { Elsewhere } from './directive-attributes.ts'
|
||||
import { attributeNestingMessage, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { attributeValue, directiveLineEscape, directivePrefix, inlineDirectiveEscape, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||
import { attributeValue, directivePrefix, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||
import { blockArgument } from '../block-directive-arguments.ts'
|
||||
import { blockDirective } from '../../adf/block-directives.ts'
|
||||
import { carryFence, carryName } from '../opaque-carry.ts'
|
||||
import { blockDirectiveForm } from '../block-directive-forms.ts'
|
||||
import { carryName } from '../opaque-carry.ts'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||
import { inlineMarkSpellingFault } from './directive-marks.ts'
|
||||
import { listBreakName, listBreakSpelling } from '../list-break.ts'
|
||||
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
|
||||
import { readVocabulary } from './directive-attributes.ts'
|
||||
import { slotLineEndingFault } from '../directive-syntax.ts'
|
||||
@@ -26,10 +26,10 @@ export function readBlockDirectiveNode(
|
||||
path: ConvertErrorPath,
|
||||
): Result<BlockDirectiveNode> {
|
||||
if (name === carryName) {
|
||||
return failure('malformed-directive', `the name ${carryName} is reserved for the opaque carry, whose block form is the ${carryFence} fence`, path)
|
||||
return failure('malformed-directive', `the name ${carryName} is reserved for the opaque carry, whose block form is the ${carryName} fence`, path)
|
||||
}
|
||||
const directive = blockDirective(name)
|
||||
if (directive === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name, directiveLineEscape), path)
|
||||
if (directive === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name), path)
|
||||
const argumentKey = blockArgument(name)
|
||||
const rest = new Map(attributes)
|
||||
rest.delete(marksAttribute)
|
||||
@@ -53,7 +53,7 @@ export function readInlineDirectiveNode(
|
||||
path: ConvertErrorPath,
|
||||
): Result<AdfNode> {
|
||||
const directive = inlineDirective(name)
|
||||
if (directive === undefined) return faulted(blockSpellingFault(name) ?? unknownDirectiveFault(name, inlineDirectiveEscape), path)
|
||||
if (directive === undefined) return faulted(blockSpellingFault(name) ?? unknownDirectiveFault(name), path)
|
||||
const slot = directive.textAttribute
|
||||
if (slot === undefined && content !== undefined) return failure('unsupported-node-shape', `${name} takes no content: this one holds some`, path)
|
||||
const elsewhere: Elsewhere | undefined = slot === undefined ? undefined : { key: slot, slot: 'content' }
|
||||
@@ -80,11 +80,8 @@ function inlineSpellingFault(name: string): ConvertFault | undefined {
|
||||
}
|
||||
|
||||
function blockSpellingFault(name: string): ConvertFault | undefined {
|
||||
if (name === listBreakName) return { code: 'unsupported-node-shape', message: `${name} takes the block form, ${listBreakSpelling}, never the inline form` }
|
||||
const directive = blockDirective(name)
|
||||
if (directive === undefined) return undefined
|
||||
const form = directive.contentModel === 'none' ? `::${name}` : `:::${name}`
|
||||
return { code: 'unsupported-node-shape', message: `${name} takes the block form, ${form}, never the inline form` }
|
||||
if (blockDirectiveForm(name) === undefined) return undefined
|
||||
return { code: 'unsupported-node-shape', message: `${name} takes the block form, ${directivePrefix}${name}, never the inline form` }
|
||||
}
|
||||
|
||||
// spec/flavour.md, Inline nodes: the slot is plain text, its adjacent nodes already merged.
|
||||
|
||||
@@ -132,30 +132,29 @@ test('reads a fenced code block, its info string the language', () => {
|
||||
})
|
||||
|
||||
test('reads the codeBlock directive body as the node content, the info string its language', () => {
|
||||
const fenced = ':::codeBlock {wrap=true}\n```rust\nfn main() {}\n```\n:::\n'
|
||||
const fenced = '!adf:codeBlock {wrap=true}\n```rust\nfn main() {}\n```\n!adf:/codeBlock\n'
|
||||
assert.deepEqual(content(markdownToAdf(fenced)), [
|
||||
{ attrs: { language: 'rust', wrap: true }, content: [text('fn main() {}')], type: 'codeBlock' },
|
||||
])
|
||||
assert.deepEqual(content(markdownToAdf(':::codeBlock {wrap=true}\n```\n```\n:::\n')), [{ attrs: { wrap: true }, type: 'codeBlock' }])
|
||||
assert.deepEqual(content(markdownToAdf(':::codeBlock {language=""}\n```\nx\n```\n:::\n')), [
|
||||
assert.deepEqual(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```\n```\n!adf:/codeBlock\n')), [{ attrs: { wrap: true }, type: 'codeBlock' }])
|
||||
assert.deepEqual(content(markdownToAdf('!adf:codeBlock {language=""}\n```\nx\n```\n!adf:/codeBlock\n')), [
|
||||
{ attrs: { language: '' }, content: [text('x')], type: 'codeBlock' },
|
||||
])
|
||||
assert.deepEqual(content(markdownToAdf(':::codeBlock {wrap=true}\n fn()\n:::\n')), [
|
||||
assert.deepEqual(content(markdownToAdf('!adf:codeBlock {wrap=true}\n fn()\n!adf:/codeBlock\n')), [
|
||||
{ attrs: { wrap: true }, content: [text('fn()')], type: 'codeBlock' },
|
||||
])
|
||||
// The body is a CommonMark fence, so its info string decodes escapes the way any other fence's does.
|
||||
assert.deepEqual(content(markdownToAdf(':::codeBlock {wrap=true}\n```\\#c\nx\n```\n:::\n')), [
|
||||
assert.deepEqual(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```\\#c\nx\n```\n!adf:/codeBlock\n')), [
|
||||
{ attrs: { language: '#c', wrap: true }, content: [text('x')], type: 'codeBlock' },
|
||||
])
|
||||
})
|
||||
|
||||
test('names the slot a codeBlock spells its language outside of', () => {
|
||||
const slot = 'unsupported-node-shape: codeBlock spells its language in the fence info string, or in the attribute where no info string carries it back'
|
||||
assert.equal(content(markdownToAdf(':::codeBlock {language=rust wrap=true}\n```\nx\n```\n:::\n')), slot)
|
||||
assert.equal(content(markdownToAdf(':::codeBlock {language=rust}\n```sql\nx\n```\n:::\n')), slot)
|
||||
assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\n```adf\nx\n```\n:::\n')), slot)
|
||||
assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\n```a\\b\nx\n```\n:::\n')), slot)
|
||||
assert.equal(content(markdownToAdf('::codeBlock {wrap=true}\n')), 'unsupported-node-shape: codeBlock spells its body in the container form, :::, never the leaf form')
|
||||
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust wrap=true}\n```\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust}\n```sql\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```carry\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```a\\b\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||
})
|
||||
|
||||
test('reads a pipe table into the header row and the body rows under it', () => {
|
||||
@@ -205,33 +204,33 @@ test('names the pipe table whose rows open with no pipe', () => {
|
||||
|
||||
test('gives back the refusal an inline body holds, never the shape check above it', () => {
|
||||
const bare = 'malformed-pipe-table: a pipe table opens every row with `|`: this one does not; \\| keeps a pipe literal text'
|
||||
assert.equal(content(markdownToAdf(':::caption\na | b\n--- | ---\n:::\n')), bare)
|
||||
assert.deepEqual(position(markdownToAdf(':::caption\na | b\n--- | ---\n:::\n')), { line: 2, offset: 11 })
|
||||
assert.equal(code(markdownToAdf(':::caption\n| a |\n:::\n')), 'malformed-pipe-table')
|
||||
assert.equal(content(markdownToAdf(':::caption\n- a\n:::\n')), 'unsupported-node-shape: caption takes one paragraph as its body: this body is not one')
|
||||
assert.equal(content(markdownToAdf('!adf:caption\na | b\n--- | ---\n!adf:/caption\n')), bare)
|
||||
assert.deepEqual(position(markdownToAdf('!adf:caption\na | b\n--- | ---\n!adf:/caption\n')), { line: 2, offset: 13 })
|
||||
assert.equal(code(markdownToAdf('!adf:caption\n| a |\n!adf:/caption\n')), 'malformed-pipe-table')
|
||||
assert.equal(content(markdownToAdf('!adf:caption\n- a\n!adf:/caption\n')), 'unsupported-node-shape: caption takes one paragraph as its body: this body is not one')
|
||||
})
|
||||
|
||||
test('reads the separator that parts two adjacent lists of one kind', () => {
|
||||
const parted = [bulletList(item(paragraph('a'))), bulletList(item(paragraph('b')))]
|
||||
assert.deepEqual(content(markdownToAdf('- a\n\n::listBreak\n\n- b\n')), parted)
|
||||
assert.deepEqual(content(markdownToAdf('- a\n::listBreak\n- b\n')), parted)
|
||||
assert.deepEqual(content(markdownToAdf('1. a\n\n::listBreak\n\n1. b\n')), [orderedList(1, item(paragraph('a'))), orderedList(1, item(paragraph('b')))])
|
||||
assert.deepEqual(content(markdownToAdf('> - a\n> ::listBreak\n> - b\n')), [quote(...parted)])
|
||||
assert.deepEqual(path(markdownToAdf('- a\n\n::listBreak\n\n- b\n\n| x |\n')), ['content', 2])
|
||||
assert.deepEqual(content(markdownToAdf('- a\n\n!adf:listBreak\n\n- b\n')), parted)
|
||||
assert.deepEqual(content(markdownToAdf('- a\n!adf:listBreak\n- b\n')), parted)
|
||||
assert.deepEqual(content(markdownToAdf('1. a\n\n!adf:listBreak\n\n1. b\n')), [orderedList(1, item(paragraph('a'))), orderedList(1, item(paragraph('b')))])
|
||||
assert.deepEqual(content(markdownToAdf('> - a\n> !adf:listBreak\n> - b\n')), [quote(...parted)])
|
||||
assert.deepEqual(path(markdownToAdf('- a\n\n!adf:listBreak\n\n- b\n\n| x |\n')), ['content', 2])
|
||||
})
|
||||
|
||||
test('refuses the list separator that parts anything else', () => {
|
||||
const parts = 'unsupported-node-shape: listBreak parts two adjacent lists of one type: this one parts something else'
|
||||
assert.equal(content(markdownToAdf('::listBreak\n')), parts)
|
||||
assert.equal(content(markdownToAdf('- a\n\n::listBreak\n')), parts)
|
||||
assert.equal(content(markdownToAdf('- a\n\n::listBreak\n\n1. b\n')), parts)
|
||||
assert.equal(content(markdownToAdf('Part.\n\n::listBreak\n\n- b\n')), parts)
|
||||
const bare = 'unsupported-node-shape: listBreak spells the bare leaf form, ::listBreak: this one spells more'
|
||||
assert.equal(content(markdownToAdf('- a\n\n::listBreak x\n\n- b\n')), bare)
|
||||
assert.equal(content(markdownToAdf('- a\n\n::listBreak {id=x}\n\n- b\n')), bare)
|
||||
assert.equal(content(markdownToAdf(':::listBreak\n- a\n:::\n')), bare)
|
||||
assert.equal(content(markdownToAdf('!adf:listBreak{}\n')), 'unsupported-node-shape: listBreak takes the block form, ::listBreak, never the inline form')
|
||||
assert.deepEqual(path(markdownToAdf('Part.\n\n::listBreak\n')), ['content', 1])
|
||||
assert.equal(content(markdownToAdf('!adf:listBreak\n')), parts)
|
||||
assert.equal(content(markdownToAdf('- a\n\n!adf:listBreak\n')), parts)
|
||||
assert.equal(content(markdownToAdf('- a\n\n!adf:listBreak\n\n1. b\n')), parts)
|
||||
assert.equal(content(markdownToAdf('Part.\n\n!adf:listBreak\n\n- b\n')), parts)
|
||||
const bare = 'unsupported-node-shape: listBreak spells the bare leaf form, !adf:listBreak: this one spells more'
|
||||
assert.equal(content(markdownToAdf('- a\n\n!adf:listBreak x\n\n- b\n')), bare)
|
||||
assert.equal(content(markdownToAdf('- a\n\n!adf:listBreak {id=x}\n\n- b\n')), bare)
|
||||
assert.equal(content(markdownToAdf('- a\n\n!adf:listBreak\n- b\n!adf:/listBreak\n')), 'malformed-directive: listBreak takes no body, so no !adf:/listBreak closes it')
|
||||
assert.equal(content(markdownToAdf('!adf:listBreak{}\n')), 'unsupported-node-shape: listBreak takes the block form, !adf:listBreak, never the inline form')
|
||||
assert.deepEqual(path(markdownToAdf('Part.\n\n!adf:listBreak\n')), ['content', 1])
|
||||
})
|
||||
|
||||
test('refuses the image a pipe cell holds no ADF node for', () => {
|
||||
@@ -252,22 +251,24 @@ test('reads an indented code block where no paragraph is open', () => {
|
||||
assert.deepEqual(content(markdownToAdf('Part.\n more\n')), [paragraph('Part. more')])
|
||||
})
|
||||
|
||||
test('claims a block-level colon run with no directive to parse it', () => {
|
||||
assert.equal(code(markdownToAdf(':::\n')), 'malformed-directive')
|
||||
assert.equal(code(markdownToAdf('::Panel\n')), 'malformed-directive')
|
||||
assert.equal(code(markdownToAdf('::panel {a=1 a=2}\n')), 'malformed-directive')
|
||||
assert.deepEqual(content(markdownToAdf(':10:30\n')), [paragraph(':10:30')])
|
||||
assert.deepEqual(content(markdownToAdf(':: two\n')), [paragraph(':: two')])
|
||||
test('claims a block line the prefix opens with no directive to parse it', () => {
|
||||
assert.equal(code(markdownToAdf('!adf:/panel\n')), 'malformed-directive')
|
||||
assert.equal(code(markdownToAdf('!adf:panel info\n')), 'malformed-directive')
|
||||
assert.equal(code(markdownToAdf('!adf:panel {a=1 a=2}\n!adf:/panel\n')), 'malformed-directive')
|
||||
assert.equal(content(markdownToAdf('!adf:Panel\n')), 'malformed-directive: an unescaped !adf: completes no directive; \\!adf: keeps the prefix literal')
|
||||
assert.deepEqual(content(markdownToAdf(':::panel info\nPart.\n:::\n')), [paragraph(':::panel info Part. :::')])
|
||||
assert.deepEqual(content(markdownToAdf('::rule\n')), [paragraph('::rule')])
|
||||
})
|
||||
|
||||
test('reads the three directive forms into the nodes the tables name', () => {
|
||||
assert.deepEqual(content(markdownToAdf('::rule {localId=a-1}\n')), [{ attrs: { localId: 'a-1' }, type: 'rule' }])
|
||||
assert.deepEqual(content(markdownToAdf('::paragraph\n')), [{ type: 'paragraph' }])
|
||||
assert.deepEqual(content(markdownToAdf(' :::panel info\nPart.\n:::\n')), [
|
||||
assert.deepEqual(content(markdownToAdf('!adf:rule {localId=a-1}\n')), [{ attrs: { localId: 'a-1' }, type: 'rule' }])
|
||||
assert.deepEqual(content(markdownToAdf('!adf:paragraph\n!adf:/paragraph\n')), [{ type: 'paragraph' }])
|
||||
assert.deepEqual(content(markdownToAdf('!adf:bulletList\n!adf:/bulletList\n')), [{ type: 'bulletList' }])
|
||||
assert.deepEqual(content(markdownToAdf(' !adf:panel info\nPart.\n !adf:/panel\n')), [
|
||||
{ attrs: { panelType: 'info' }, content: [paragraph('Part.')], type: 'panel' },
|
||||
])
|
||||
assert.deepEqual(content(markdownToAdf(':::blockquote {localId=a-1}\n:::\n')), [{ attrs: { localId: 'a-1' }, type: 'blockquote' }])
|
||||
assert.deepEqual(content(markdownToAdf(':::heading {level=2 localId=a-1}\nPart.\n:::\n')), [
|
||||
assert.deepEqual(content(markdownToAdf('!adf:blockquote {localId=a-1}\n!adf:/blockquote\n')), [{ attrs: { localId: 'a-1' }, type: 'blockquote' }])
|
||||
assert.deepEqual(content(markdownToAdf('!adf:heading {level=2 localId=a-1}\nPart.\n!adf:/heading\n')), [
|
||||
{ attrs: { level: 2, localId: 'a-1' }, content: [text('Part.')], type: 'heading' },
|
||||
])
|
||||
assert.deepEqual(content(markdownToAdf('Part!adf:hardBreak{}.\n')), [{ content: [text('Part'), hardBreak(), text('.')], type: 'paragraph' }])
|
||||
@@ -275,53 +276,70 @@ test('reads the three directive forms into the nodes the tables name', () => {
|
||||
|
||||
test('names the directive form a node CommonMark spells refuses', () => {
|
||||
const named = (type: string): string => `unsupported-node-shape: ${type} takes the CommonMark spelling, not the directive form`
|
||||
assert.equal(content(markdownToAdf('::rule\n')), named('rule'))
|
||||
assert.equal(content(markdownToAdf(':::blockquote\nPart.\n:::\n')), named('blockquote'))
|
||||
assert.equal(content(markdownToAdf(':::heading {level=2}\nPart.\n:::\n')), named('heading'))
|
||||
assert.equal(content(markdownToAdf(':::paragraph\nPart.\n:::\n')), named('paragraph'))
|
||||
assert.equal(content(markdownToAdf('::::bulletList\n:::listItem\nPart.\n:::\n::::\n')), named('bulletList'))
|
||||
assert.equal(content(markdownToAdf('!adf:rule\n')), named('rule'))
|
||||
assert.equal(content(markdownToAdf('!adf:blockquote\nPart.\n!adf:/blockquote\n')), named('blockquote'))
|
||||
assert.equal(content(markdownToAdf('!adf:heading {level=2}\nPart.\n!adf:/heading\n')), named('heading'))
|
||||
assert.equal(content(markdownToAdf('!adf:paragraph\nPart.\n!adf:/paragraph\n')), named('paragraph'))
|
||||
assert.equal(content(markdownToAdf('!adf:bulletList\n!adf:listItem\nPart.\n!adf:/listItem\n!adf:/bulletList\n')), named('bulletList'))
|
||||
// The item whose first line reads back as a thematic break keeps the directive form the emitter falls back to.
|
||||
assert.deepEqual(content(markdownToAdf('::::bulletList\n:::listItem\n---\n:::\n::::\n')), [bulletList(item({ type: 'rule' }))])
|
||||
assert.deepEqual(content(markdownToAdf('!adf:bulletList\n!adf:listItem\n---\n!adf:/listItem\n!adf:/bulletList\n')), [bulletList(item({ type: 'rule' }))])
|
||||
})
|
||||
|
||||
// The spelling the emitter refuses gives the emitter's own error, never a second name for it.
|
||||
test('gives back the refusal the CommonMark spelling itself raises', () => {
|
||||
const destination = ':::blockquote\n[t](https://example.com/a\\b)\n:::\n'
|
||||
const destination = '!adf:blockquote\n[t](https://example.com/a\\b)\n!adf:/blockquote\n'
|
||||
assert.equal(content(markdownToAdf(destination)), 'unspellable-link: no canonical escape spells a backslash in a link destination')
|
||||
})
|
||||
|
||||
test('names the directive name no node reads back to', () => {
|
||||
assert.equal(code(markdownToAdf(':::widget info\nx\n:::\n')), 'unknown-directive-name')
|
||||
assert.equal(content(markdownToAdf('::widget\n')), 'unknown-directive-name: the directive name widget reads back to no node; \\::: keeps the line literal text')
|
||||
assert.equal(code(markdownToAdf('!adf:widget info\nx\n!adf:/widget\n')), 'unknown-directive-name')
|
||||
assert.equal(content(markdownToAdf('!adf:widget\n')), 'unknown-directive-name: the directive name widget reads back to no node; \\!adf: keeps the prefix literal')
|
||||
assert.equal(content(markdownToAdf('!adf:widget[x]\n')), 'unknown-directive-name: the directive name widget reads back to no node; \\!adf: keeps the prefix literal')
|
||||
assert.equal(content(markdownToAdf('ratio a!adf:b[c]{d}\n')), 'malformed-directive: an attribute reads key=value, the value bare or double-quoted: this one does not; \\!adf: keeps the prefix literal')
|
||||
assert.deepEqual(path(markdownToAdf('Part.\n\n::widget\n')), ['content', 1])
|
||||
assert.equal(content(markdownToAdf('Part.\n:::x\n')), 'malformed-directive: a container fenced with 3 colons is unclosed')
|
||||
assert.deepEqual(path(markdownToAdf('Part.\n:::x\n')), ['content', 1])
|
||||
assert.deepEqual(path(markdownToAdf('Part.\n\n!adf:widget\n')), ['content', 1])
|
||||
})
|
||||
|
||||
test('names the container no closer closes inside the block holding it', () => {
|
||||
const unclosed = (name: string): string => `malformed-directive: the ${name} container is unclosed: no !adf:/${name} follows inside the block holding it`
|
||||
assert.equal(content(markdownToAdf('Part.\n!adf:expand\n')), unclosed('expand'))
|
||||
assert.deepEqual(path(markdownToAdf('Part.\n!adf:expand\n')), ['content', 1])
|
||||
assert.equal(content(markdownToAdf('!adf:bulletList\n')), unclosed('bulletList'))
|
||||
assert.equal(content(markdownToAdf('- !adf:panel info\n Part.\n!adf:/panel\n')), unclosed('panel'))
|
||||
assert.equal(content(markdownToAdf('!adf:panel info\n!adf:expand\nPart.\n!adf:/panel\n')), unclosed('expand'))
|
||||
assert.deepEqual(path(markdownToAdf('!adf:panel info\n!adf:expand\nPart.\n!adf:/panel\n')), ['content', 0, 'content', 0])
|
||||
})
|
||||
|
||||
test('names the closer that finds no container open where it stands', () => {
|
||||
const unopened = (name: string): string => `malformed-directive: the closer !adf:/${name} closes no ${name} container open where it stands; \\!adf: keeps the prefix literal`
|
||||
assert.equal(content(markdownToAdf('Part.\n\n!adf:/panel\n')), unopened('panel'))
|
||||
assert.equal(content(markdownToAdf('!adf:panel info\n> !adf:/panel\n!adf:/panel\n')), unopened('panel'))
|
||||
assert.equal(content(markdownToAdf('!adf:panel info\n- !adf:/panel\n!adf:/panel\n')), unopened('panel'))
|
||||
assert.equal(content(markdownToAdf('!adf:panel info\nPart.\n!adf:/expand\n!adf:/panel\n')), unopened('expand'))
|
||||
assert.equal(content(markdownToAdf('!adf:rule {localId=a-1}\nPart.\n!adf:/rule\n')), 'malformed-directive: rule takes no body, so no !adf:/rule closes it')
|
||||
})
|
||||
|
||||
test('names the position a directive name the other one spells belongs to', () => {
|
||||
assert.equal(content(markdownToAdf(':::em\na\n:::\n')), 'unsupported-node-shape: em is spelled _x_, never as a block directive')
|
||||
assert.equal(content(markdownToAdf('::underline\n')), 'unsupported-node-shape: underline is spelled !adf:underline[…], never as a block directive')
|
||||
assert.equal(content(markdownToAdf('::text {text=" "}\n')), 'unsupported-node-shape: text takes the inline form, !adf:text{…}, never the block form')
|
||||
assert.equal(content(markdownToAdf('::date {timestamp=1}\n')), 'unsupported-node-shape: date takes the inline form, !adf:date{…}, never the block form')
|
||||
assert.equal(content(markdownToAdf('!adf:paragraph[a]\n')), 'unsupported-node-shape: paragraph takes the block form, :::paragraph, never the inline form')
|
||||
assert.equal(content(markdownToAdf('!adf:rule[a]\n')), 'unsupported-node-shape: rule takes the block form, ::rule, never the inline form')
|
||||
assert.equal(code(markdownToAdf(':::widget\na\n:::\n')), 'unknown-directive-name')
|
||||
assert.equal(content(markdownToAdf('!adf:em\na\n!adf:/em\n')), 'unsupported-node-shape: em is spelled _x_, never as a block directive')
|
||||
assert.equal(content(markdownToAdf('!adf:underline\n')), 'unsupported-node-shape: underline is spelled !adf:underline[…], never as a block directive')
|
||||
assert.equal(content(markdownToAdf('!adf:text {text=" "}\n')), 'unsupported-node-shape: text takes the inline form, !adf:text{…}, never the block form')
|
||||
assert.equal(content(markdownToAdf('!adf:date {timestamp=1}\n')), 'unsupported-node-shape: date takes the inline form, !adf:date{…}, never the block form')
|
||||
assert.equal(content(markdownToAdf('!adf:paragraph[a]\n')), 'unsupported-node-shape: paragraph takes the block form, !adf:paragraph, never the inline form')
|
||||
assert.equal(content(markdownToAdf('!adf:rule[a]\n')), 'unsupported-node-shape: rule takes the block form, !adf:rule, never the inline form')
|
||||
assert.equal(code(markdownToAdf('!adf:widget\na\n!adf:/widget\n')), 'unknown-directive-name')
|
||||
assert.equal(code(markdownToAdf('!adf:widget[a]\n')), 'unknown-directive-name')
|
||||
})
|
||||
|
||||
test('names the reserved carry name a block directive spells', () => {
|
||||
const reserved = 'malformed-directive: the name carry is reserved for the opaque carry, whose block form is the adf fence'
|
||||
assert.equal(content(markdownToAdf('::carry\n')), reserved)
|
||||
assert.equal(content(markdownToAdf(':::carry\nx\n:::\n')), reserved)
|
||||
assert.deepEqual(content(markdownToAdf('```adfx\nx\n```\n')), [{ attrs: { language: 'adfx' }, content: [text('x')], type: 'codeBlock' }])
|
||||
const reserved = 'malformed-directive: the name carry is reserved for the opaque carry, whose block form is the carry fence'
|
||||
assert.equal(content(markdownToAdf('!adf:carry\n')), reserved)
|
||||
assert.equal(content(markdownToAdf('!adf:carry\nx\n!adf:/carry\n')), reserved)
|
||||
assert.deepEqual(content(markdownToAdf('```adf\nx\n```\n')), [{ attrs: { language: 'adf' }, content: [text('x')], type: 'codeBlock' }])
|
||||
})
|
||||
|
||||
const carried = '!adf:carry{json="{\\"type\\":\\"placeholder\\"}"}'
|
||||
|
||||
test('reads the adf fence back to the node its JSON holds', () => {
|
||||
assert.deepEqual(content(markdownToAdf('```adf\n{\n "attrs": {\n "url": "https://example.com/x"\n },\n "type": "blockCard"\n}\n```\n')), [
|
||||
test('reads the carry fence back to the node its JSON holds', () => {
|
||||
assert.deepEqual(content(markdownToAdf('```carry\n{\n "attrs": {\n "url": "https://example.com/x"\n },\n "type": "blockCard"\n}\n```\n')), [
|
||||
{ attrs: { url: 'https://example.com/x' }, type: 'blockCard' },
|
||||
])
|
||||
})
|
||||
@@ -334,22 +352,22 @@ test('reads the inline carry back to the node its json attribute holds', () => {
|
||||
|
||||
test('names the invalid JSON no opaque carry holds', () => {
|
||||
const invalid = 'malformed-directive: the opaque carry holds invalid JSON'
|
||||
assert.equal(content(markdownToAdf('```adf\n{"type":\n```\n')), invalid)
|
||||
assert.equal(content(markdownToAdf('```adf\n```\n')), invalid)
|
||||
assert.equal(content(markdownToAdf('```carry\n{"type":\n```\n')), invalid)
|
||||
assert.equal(content(markdownToAdf('```carry\n```\n')), invalid)
|
||||
assert.equal(content(markdownToAdf('!adf:carry{json="{"}\n')), invalid)
|
||||
assert.equal(content(markdownToAdf('!adf:carry{json=abc}\n')), invalid)
|
||||
})
|
||||
|
||||
test('names the canonical spelling a carried JSON reads alone', () => {
|
||||
const canonically = "unsupported-node-shape: the opaque carry spells its node's JSON canonically: "
|
||||
assert.equal(content(markdownToAdf('```adf\n{"type":"blockCard"}\n```\n')), `${canonically}two-space indent, keys sorted`)
|
||||
assert.equal(content(markdownToAdf('```carry\n{"type":"blockCard"}\n```\n')), `${canonically}two-space indent, keys sorted`)
|
||||
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"type\\": \\"blockCard\\"}"}\n')), `${canonically}compact, keys sorted`)
|
||||
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"type\\":\\"blockCard\\",\\"attrs\\":{}}"}\n')), `${canonically}compact, keys sorted`)
|
||||
})
|
||||
|
||||
test('names the node JSON an opaque carry restores alone', () => {
|
||||
const node = "unsupported-node-shape: the opaque carry holds one ADF node's JSON: this JSON is no ADF node"
|
||||
assert.equal(content(markdownToAdf('```adf\n[]\n```\n')), node)
|
||||
assert.equal(content(markdownToAdf('```carry\n[]\n```\n')), node)
|
||||
assert.equal(content(markdownToAdf('!adf:carry{json=null}\n')), node)
|
||||
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"kind\\":\\"x\\"}"}\n')), node)
|
||||
})
|
||||
@@ -363,7 +381,7 @@ test('names the shape the inline carry reads alone', () => {
|
||||
|
||||
test('holds a carried JSON value to the nesting its position leaves', () => {
|
||||
const nested = (levels: number): string => `${'['.repeat(levels)}${']'.repeat(levels)}`
|
||||
const fence = (prefix: string, levels: number): string => `${prefix}\`\`\`adf\n${prefix}${nested(levels)}\n${prefix}\`\`\`\n`
|
||||
const fence = (prefix: string, levels: number): string => `${prefix}\`\`\`carry\n${prefix}${nested(levels)}\n${prefix}\`\`\`\n`
|
||||
const deeper = (levels: number): string => `unsupported-nesting-depth: a carried node's JSON nests deeper than the ${levels} levels its position leaves`
|
||||
assert.equal(content(markdownToAdf(`!adf:carry{json="${nested(largestNesting + 2)}"}\n`)), deeper(largestNesting))
|
||||
assert.equal(
|
||||
@@ -376,7 +394,7 @@ test('holds a carried JSON value to the nesting its position leaves', () => {
|
||||
test('names the number no JSON spelling carries in an opaque carry', () => {
|
||||
const named = 'unsupported-node-shape: the opaque carry holds a number JSON cannot spell'
|
||||
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"attrs\\":{\\"width\\":1e999},\\"type\\":\\"blockCard\\"}"}\n')), named)
|
||||
assert.equal(content(markdownToAdf('```adf\n1e999\n```\n')), named)
|
||||
assert.equal(content(markdownToAdf('```carry\n1e999\n```\n')), named)
|
||||
})
|
||||
|
||||
test('names the mark spelling no opaque carry sits inside', () => {
|
||||
@@ -401,37 +419,37 @@ test('keeps the carry a mark spelling does not wrap', () => {
|
||||
})
|
||||
|
||||
test('reads each attribute value as the type its section assigns', () => {
|
||||
assert.deepEqual(content(markdownToAdf('::media {height=10 id=a-1 type=file url="/x y" width="20.5"}\n')), [
|
||||
assert.deepEqual(content(markdownToAdf('!adf:media {height=10 id=a-1 type=file url="/x y" width="20.5"}\n')), [
|
||||
{ attrs: { height: 10, id: 'a-1', type: 'file', url: '/x y', width: 20.5 }, type: 'media' },
|
||||
])
|
||||
assert.deepEqual(content(markdownToAdf(':::table {isNumberColumnEnabled=true}\n:::\n')), [{ attrs: { isNumberColumnEnabled: true }, type: 'table' }])
|
||||
assert.deepEqual(content(markdownToAdf(':::tableCell {colwidth="[340,420]"}\n:::\n')), [{ attrs: { colwidth: [340, 420] }, type: 'tableCell' }])
|
||||
assert.deepEqual(content(markdownToAdf('::rule {localId=a-1}\n')), [{ attrs: { localId: 'a-1' }, type: 'rule' }])
|
||||
assert.deepEqual(content(markdownToAdf('!adf:table {isNumberColumnEnabled=true}\n!adf:/table\n')), [{ attrs: { isNumberColumnEnabled: true }, type: 'table' }])
|
||||
assert.deepEqual(content(markdownToAdf('!adf:tableCell {colwidth="[340,420]"}\n!adf:/tableCell\n')), [{ attrs: { colwidth: [340, 420] }, type: 'tableCell' }])
|
||||
assert.deepEqual(content(markdownToAdf('!adf:rule {localId=a-1}\n')), [{ attrs: { localId: 'a-1' }, type: 'rule' }])
|
||||
})
|
||||
|
||||
test('reads the reserved marks key as the node array it spells', () => {
|
||||
assert.deepEqual(content(markdownToAdf('::rule {marks="[{\\"type\\":\\"em\\"}]"}\n')), [{ marks: [em], type: 'rule' }])
|
||||
assert.deepEqual(content(markdownToAdf('::rule {localId=a-1 marks="[{\\"attrs\\":{\\"mode\\":\\"wide\\"},\\"type\\":\\"breakout\\"}]"}\n')), [
|
||||
assert.deepEqual(content(markdownToAdf('!adf:rule {marks="[{\\"type\\":\\"em\\"}]"}\n')), [{ marks: [em], type: 'rule' }])
|
||||
assert.deepEqual(content(markdownToAdf('!adf:rule {localId=a-1 marks="[{\\"attrs\\":{\\"mode\\":\\"wide\\"},\\"type\\":\\"breakout\\"}]"}\n')), [
|
||||
{ attrs: { localId: 'a-1' }, marks: [{ attrs: { mode: 'wide' }, type: 'breakout' }], type: 'rule' },
|
||||
])
|
||||
})
|
||||
|
||||
test('names the marks key no marks array reads back from', () => {
|
||||
const named = 'unsupported-node-shape: the marks attribute of rule is its marks array in canonical JSON: this one is not'
|
||||
assert.equal(content(markdownToAdf('::rule {marks="[]"}\n')), named)
|
||||
assert.equal(content(markdownToAdf('::rule {marks="[1]"}\n')), named)
|
||||
assert.equal(content(markdownToAdf('::rule {marks="{}"}\n')), named)
|
||||
assert.equal(content(markdownToAdf('::rule {marks=x}\n')), named)
|
||||
assert.equal(content(markdownToAdf('::rule {marks="[{\\"attrs\\":{},\\"type\\":\\"em\\"}]"}\n')), named)
|
||||
assert.equal(content(markdownToAdf('!adf:rule {marks="[]"}\n')), named)
|
||||
assert.equal(content(markdownToAdf('!adf:rule {marks="[1]"}\n')), named)
|
||||
assert.equal(content(markdownToAdf('!adf:rule {marks="{}"}\n')), named)
|
||||
assert.equal(content(markdownToAdf('!adf:rule {marks=x}\n')), named)
|
||||
assert.equal(content(markdownToAdf('!adf:rule {marks="[{\\"attrs\\":{},\\"type\\":\\"em\\"}]"}\n')), named)
|
||||
})
|
||||
|
||||
test('names the attribute a node holds no reading for', () => {
|
||||
assert.equal(content(markdownToAdf('::rule {bogus=1}\n')), 'unsupported-node-shape: rule holds no bogus attribute: this one spells it')
|
||||
assert.equal(content(markdownToAdf('::media {width=wide}\n')), 'unsupported-node-shape: the width attribute of media is no number')
|
||||
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')
|
||||
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('!adf:rule {bogus=1}\n')), 'unsupported-node-shape: rule holds no bogus attribute: this one spells it')
|
||||
assert.equal(content(markdownToAdf('!adf:media {width=wide}\n')), 'unsupported-node-shape: the width attribute of media is no number')
|
||||
assert.equal(content(markdownToAdf('!adf:table {isNumberColumnEnabled=yes}\n!adf:/table\n')), 'unsupported-node-shape: the isNumberColumnEnabled attribute of table is no boolean')
|
||||
assert.equal(content(markdownToAdf('!adf:media {width=true}\n')), 'unsupported-node-shape: the width attribute of media is no number')
|
||||
assert.equal(content(markdownToAdf('!adf:tableCell {colwidth="[340,"}\n!adf:/tableCell\n')), 'unsupported-node-shape: the colwidth attribute of tableCell is no json')
|
||||
assert.equal(content(markdownToAdf('!adf:panel info {panelType=note}\nx\n!adf:/panel\n')), 'unsupported-node-shape: panel spells its panelType attribute as the directive argument, never in {attrs}')
|
||||
assert.equal(content(markdownToAdf('Part !adf:mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot, never in {attrs}')
|
||||
})
|
||||
|
||||
@@ -439,27 +457,24 @@ test('names the depth an attribute value nests past, never the kind the JSON rea
|
||||
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 an attribute 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(`::rule {marks="[{\\"attrs\\":{\\"deep\\":${nested(largestNesting - 2)}},\\"type\\":\\"em\\"}]"}\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')
|
||||
assert.equal(content(markdownToAdf(`!adf:tableCell {colwidth="${nested(largestNesting + 1)}"}\n!adf:/tableCell\n`)), deeper('colwidth', 'tableCell'))
|
||||
assert.equal(content(markdownToAdf(`!adf:rule {marks="${nested(largestNesting + 1)}"}\n`)), deeper('marks', 'rule'))
|
||||
assert.equal(content(markdownToAdf(`!adf:rule {marks="[{\\"attrs\\":{\\"deep\\":${nested(largestNesting - 2)}},\\"type\\":\\"em\\"}]"}\n`)), deeper('marks', 'rule'))
|
||||
assert.equal(content(markdownToAdf(`!adf: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')
|
||||
assert.equal(content(markdownToAdf(':::tableCell {colwidth="[340, 420]"}\n:::\n')), 'unsupported-node-shape: tableCell spells its colwidth attribute as colwidth="[340,420]"')
|
||||
assert.equal(content(markdownToAdf('!adf:rule {localId="a-1"}\n')), 'unsupported-node-shape: rule spells its localId attribute as localId=a-1')
|
||||
assert.equal(content(markdownToAdf('!adf:media {width="20.0"}\n')), 'unsupported-node-shape: media spells its width attribute as width=20')
|
||||
assert.equal(content(markdownToAdf('!adf:tableCell {colwidth="[340, 420]"}\n!adf:/tableCell\n')), 'unsupported-node-shape: tableCell spells its colwidth attribute as colwidth="[340,420]"')
|
||||
})
|
||||
|
||||
test('names the argument and the body a node takes no reading for', () => {
|
||||
assert.equal(content(markdownToAdf('::rule x\n')), 'unsupported-node-shape: rule takes no argument: this one spells one')
|
||||
assert.equal(content(markdownToAdf(':::rule\nPart.\n:::\n')), 'unsupported-node-shape: rule holds no content: this one holds some')
|
||||
assert.equal(content(markdownToAdf('::bulletList\n')), 'unsupported-node-shape: bulletList spells its body in the container form, :::, never the leaf form')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\n:::\n')), 'unsupported-node-shape: an empty paragraph takes the leaf form, ::, never an empty container')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\nOne.\n\nTwo.\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body: this body is not one')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\n---\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body: this body is not one')
|
||||
assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\nx\n:::\n')), 'unsupported-node-shape: codeBlock takes one code block as its body: this body is not one')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\n\n:::\n')), 'unmappable-image: no ADF node carries an image inside a paragraph')
|
||||
assert.equal(content(markdownToAdf('!adf:rule x\n')), 'unsupported-node-shape: rule takes no argument: this one spells one')
|
||||
assert.equal(content(markdownToAdf('!adf:paragraph\nOne.\n\nTwo.\n!adf:/paragraph\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body: this body is not one')
|
||||
assert.equal(content(markdownToAdf('!adf:paragraph\n---\n!adf:/paragraph\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body: this body is not one')
|
||||
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\nx\n!adf:/codeBlock\n')), 'unsupported-node-shape: codeBlock takes one code block as its body: this body is not one')
|
||||
assert.equal(content(markdownToAdf('!adf:paragraph\n\n!adf:/paragraph\n')), 'unmappable-image: no ADF node carries an image inside a paragraph')
|
||||
assert.equal(content(markdownToAdf('Part !adf:date[now]{timestamp=1}.\n')), 'unsupported-node-shape: date takes no content: this one holds some')
|
||||
})
|
||||
|
||||
@@ -500,12 +515,12 @@ test('refuses the raw HTML no element mapping carries', () => {
|
||||
assert.equal(code(markdownToAdf('<div\n')), 'unmappable-html')
|
||||
assert.equal(code(markdownToAdf('<?php\n')), 'unmappable-html')
|
||||
assert.deepEqual(path(markdownToAdf('Part.\n\n<div>\n')), ['content', 1])
|
||||
assert.equal(code(markdownToAdf('<div>\nx\n\n:::\n')), 'unmappable-html')
|
||||
assert.equal(code(markdownToAdf('<div>\nx\n\n!adf:/panel\n')), 'unmappable-html')
|
||||
assert.equal(code(markdownToAdf('<div>\n- x\n</div>\n')), 'unmappable-html')
|
||||
})
|
||||
|
||||
test('swallows an HTML block ahead of the claim a line inside it would make', () => {
|
||||
assert.equal(code(markdownToAdf('<!--\n:::\n-->\n')), 'unmappable-html')
|
||||
assert.equal(code(markdownToAdf('<!--\n!adf:/panel\n-->\n')), 'unmappable-html')
|
||||
assert.equal(code(markdownToAdf('<div>\n| x |\n</div>\n')), 'unmappable-html')
|
||||
})
|
||||
|
||||
@@ -514,15 +529,15 @@ test('names the line and the offset in the input a refusal sits at, the innermos
|
||||
assert.deepEqual(position(markdownToAdf('Part.\n\n<div>\n')), { line: 3, offset: 7 })
|
||||
assert.deepEqual(position(markdownToAdf('> Part.\n>\n> a <span>b</span>\n')), { line: 3, offset: 10 })
|
||||
assert.deepEqual(position(markdownToAdf('- Part.\n- a <span>b</span>\n')), { line: 2, offset: 8 })
|
||||
assert.deepEqual(position(markdownToAdf('Part.\n\n:::panel info\nMore.\n')), { line: 3, offset: 7 })
|
||||
assert.deepEqual(position(markdownToAdf('Part.\n\n!adf:panel info\nMore.\n')), { line: 3, offset: 7 })
|
||||
assert.deepEqual(position(markdownToAdf('x\n\na <span>b</span>\n===\n')), { line: 3, offset: 3 })
|
||||
assert.deepEqual(position(markdownToAdf('x\n\n```adf\n{\n```\n')), { line: 3, offset: 3 })
|
||||
assert.deepEqual(position(markdownToAdf('x\n\n```carry\n{\n```\n')), { line: 3, offset: 3 })
|
||||
assert.deepEqual(position(markdownToAdf('x\n\n| a |\n')), { line: 3, offset: 3 })
|
||||
assert.deepEqual(position(markdownToAdf('a\nb <span>c</span>\n')), { line: 1, offset: 0 })
|
||||
assert.deepEqual(position(markdownToAdf('Part.\r\n\r\n<div>\r\n')), { line: 3, offset: 9 })
|
||||
assert.deepEqual(position(markdownToAdf('a\u0000b\n\n<div>\n')), { line: 3, offset: 5 })
|
||||
assert.deepEqual(position(markdownToAdf(':::caption\na <span>b</span>\n:::\n')), { line: 2, offset: 11 })
|
||||
assert.deepEqual(position(markdownToAdf('x\n\n:::caption\n- a\n:::\n')), { line: 3, offset: 3 })
|
||||
assert.deepEqual(position(markdownToAdf('!adf:caption\na <span>b</span>\n!adf:/caption\n')), { line: 2, offset: 13 })
|
||||
assert.deepEqual(position(markdownToAdf('x\n\n!adf:caption\n- a\n!adf:/caption\n')), { line: 3, offset: 3 })
|
||||
})
|
||||
|
||||
test('names the line the text a paragraph keeps starts on, never a definition line it gave up', () => {
|
||||
@@ -530,7 +545,7 @@ test('names the line the text a paragraph keeps starts on, never a definition li
|
||||
assert.deepEqual(position(markdownToAdf('[a]: /a\n[b]: /b\n[c]: /c\n[d]: /d\n<span>x</span>\n')), { line: 5, offset: 32 })
|
||||
assert.deepEqual(position(markdownToAdf('[a]:\n<the url>\n"Title"\n<span>b</span>\n')), { line: 4, offset: 23 })
|
||||
assert.deepEqual(position(markdownToAdf('> [a]: /url\n> <span>b</span>\n')), { line: 2, offset: 12 })
|
||||
assert.deepEqual(position(markdownToAdf(':::caption\n[a]: /url\n<span>b</span>\n:::\n')), { line: 3, offset: 21 })
|
||||
assert.deepEqual(position(markdownToAdf('!adf:caption\n[a]: /url\n<span>b</span>\n!adf:/caption\n')), { line: 3, offset: 23 })
|
||||
})
|
||||
|
||||
test('gives up the link reference definitions a paragraph opens with', () => {
|
||||
@@ -626,13 +641,13 @@ test('folds a lazy continuation into the paragraph the container holds', () => {
|
||||
})
|
||||
|
||||
test('ends a lazy continuation at a claimed line', () => {
|
||||
assert.equal(code(markdownToAdf('> Part.\n:::\n')), 'malformed-directive')
|
||||
assert.deepEqual(path(markdownToAdf('> Part.\n:::\n')), ['content', 1])
|
||||
assert.equal(code(markdownToAdf('> Part.\n!adf:/panel\n')), 'malformed-directive')
|
||||
assert.deepEqual(path(markdownToAdf('> Part.\n!adf:/panel\n')), ['content', 1])
|
||||
assert.equal(code(markdownToAdf('- Part.\n| x |\n')), 'malformed-pipe-table')
|
||||
})
|
||||
|
||||
test('names the block the claim inside a container opens', () => {
|
||||
assert.deepEqual(path(markdownToAdf('> Part.\n>\n> :::x\n')), ['content', 0, 'content', 1])
|
||||
assert.deepEqual(path(markdownToAdf('> Part.\n>\n> !adf:expand\n')), ['content', 0, 'content', 1])
|
||||
assert.deepEqual(path(markdownToAdf('- Part.\n- | x |\n')), ['content', 0, 'content', 1, 'content', 0])
|
||||
})
|
||||
|
||||
@@ -670,9 +685,9 @@ test('reads a code span, its content literal', () => {
|
||||
assert.deepEqual(content(markdownToAdf('` `\n')), [{ content: [codeSpan(' ')], type: 'paragraph' }])
|
||||
assert.deepEqual(content(markdownToAdf('`a\nb`\n')), [{ content: [codeSpan('a b')], type: 'paragraph' }])
|
||||
assert.deepEqual(content(markdownToAdf('`foo``bar`\n')), [{ content: [codeSpan('foo``bar')], type: 'paragraph' }])
|
||||
assert.deepEqual(content(markdownToAdf('`:::panel` `~~x~~` `\\*` `&`\n')), [
|
||||
assert.deepEqual(content(markdownToAdf('`!adf:/panel` `~~x~~` `\\*` `&`\n')), [
|
||||
{
|
||||
content: [codeSpan(':::panel'), text(' '), codeSpan('~~x~~'), text(' '), codeSpan('\\*'), text(' '), codeSpan('&')],
|
||||
content: [codeSpan('!adf:/panel'), text(' '), codeSpan('~~x~~'), text(' '), codeSpan('\\*'), text(' '), codeSpan('&')],
|
||||
type: 'paragraph',
|
||||
},
|
||||
])
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Block, DirectiveBlock } from './blocks.ts'
|
||||
import type { BlockDirectiveNode } from './directive-nodes.ts'
|
||||
import type { ConvertFault } from '../../result.ts'
|
||||
import type { LinkDefinitions } from './inline-content.ts'
|
||||
import { carryFence, readCarriedBlock } from '../opaque-carry.ts'
|
||||
import { carryName, readCarriedBlock } from '../opaque-carry.ts'
|
||||
import { commonMarkSpelling } from '../emit/adf-to-markdown.ts'
|
||||
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result, type SourcePosition } from '../../result.ts'
|
||||
import { languageSlot } from '../code-language.ts'
|
||||
@@ -43,7 +43,7 @@ function blockNodes(blocks: readonly Block[], definitions: LinkDefinitions, path
|
||||
|
||||
// spec/flavour.md, Directives: the separator builds no node, so only the pair it parts spells it.
|
||||
function listBreakFault(block: DirectiveBlock, previous: Block | undefined, next: Block | undefined): ConvertFault | undefined {
|
||||
if (block.blocks !== undefined || block.argument !== undefined || block.attributes.size > 0) {
|
||||
if (block.argument !== undefined || block.attributes.size > 0) {
|
||||
return unsupportedNodeShape(`${listBreakName} spells the bare leaf form, ${listBreakSpelling}: this one spells more`)
|
||||
}
|
||||
if (previous?.kind !== 'bulletList' && previous?.kind !== 'orderedList') return partsFault()
|
||||
@@ -94,14 +94,10 @@ function directiveNode(block: DirectiveBlock, definitions: LinkDefinitions, path
|
||||
|
||||
function directiveBody(read: BlockDirectiveNode, blocks: Block[] | undefined, definitions: LinkDefinitions, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
||||
const { contentModel, node } = read
|
||||
if (blocks === undefined) {
|
||||
if (contentModel === 'none' || contentModel === 'inline') return success(node)
|
||||
return failure('unsupported-node-shape', `${node.type} spells its body in the container form, :::, never the leaf form`, path)
|
||||
}
|
||||
if (contentModel === 'none') return failure('unsupported-node-shape', `${node.type} holds no content: this one holds some`, path)
|
||||
if (blocks === undefined) return success(node)
|
||||
if (contentModel === 'code') return codeDirectiveNode(node, blocks, path)
|
||||
if (contentModel === 'block') return containerNode(node, blocks, definitions, path, depth)
|
||||
return inlineBodyNode(node, blocks, definitions, path)
|
||||
if (contentModel === 'inline') return inlineBodyNode(node, blocks, definitions, path)
|
||||
return containerNode(node, blocks, definitions, path, depth)
|
||||
}
|
||||
|
||||
function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: ConvertErrorPath): Result<AdfNode> {
|
||||
@@ -133,7 +129,7 @@ function tableNode(rows: readonly string[][], definitions: LinkDefinitions, path
|
||||
}
|
||||
|
||||
function inlineBodyNode(node: AdfNode, blocks: readonly Block[], definitions: LinkDefinitions, path: ConvertErrorPath): Result<AdfNode> {
|
||||
if (blocks.length === 0) return failure('unsupported-node-shape', `an empty ${node.type} takes the leaf form, ::, never an empty container`, path)
|
||||
if (blocks.length === 0) return success(node)
|
||||
const only = blocks.length === 1 ? blocks[0] : undefined
|
||||
if (only?.kind === 'fault') return positioned(faulted(only.fault, path), only.position)
|
||||
if (only?.kind !== 'paragraph') return failure('unsupported-node-shape', `${node.type} takes one paragraph as its body: this body is not one`, path)
|
||||
@@ -161,7 +157,7 @@ function listNode(node: AdfNode, items: readonly Block[][], definitions: LinkDef
|
||||
}
|
||||
|
||||
function codeBlockNode(language: string, text: string, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
||||
if (language === carryFence) {
|
||||
if (language === carryName) {
|
||||
const carried = readCarriedBlock(text, depth)
|
||||
if (carried.fault !== undefined) return faulted(carried.fault, path)
|
||||
return success(carried.value)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DirectiveSpan, Read } from './directive-syntax.ts'
|
||||
import { readSoleStringAttribute, spellAttributes, spellLeafDirective, spellStringAttribute, unsupportedNodeShape } from './directive-syntax.ts'
|
||||
import { readSoleStringAttribute, spellAttributes, spellInlineLeafDirective, spellStringAttribute, unsupportedNodeShape } from './directive-syntax.ts'
|
||||
|
||||
const name = 'text'
|
||||
const whitespaceRun = /^(?:[ \t]+|\n+)$/
|
||||
@@ -7,7 +7,7 @@ const whitespaceRun = /^(?:[ \t]+|\n+)$/
|
||||
export const textDirectiveName = name
|
||||
|
||||
export function spellTextDirective(text: string): string {
|
||||
return spellLeafDirective(name, spellAttributes([[name, spellStringAttribute(text)]]))
|
||||
return spellInlineLeafDirective(name, spellAttributes([[name, spellStringAttribute(text)]]))
|
||||
}
|
||||
|
||||
export function readTextDirective(span: DirectiveSpan): Read<string> | undefined {
|
||||
|
||||
Reference in New Issue
Block a user