Answer the stability pass: the slot a line ending reached, and the text an image description dropped
CI / gate (push) Successful in 8s
CI / gate (push) Successful in 8s
This commit is contained in:
@@ -157,7 +157,7 @@
|
|||||||
{
|
{
|
||||||
"content": [
|
"content": [
|
||||||
{
|
{
|
||||||
"text": "Spare",
|
"text": " Spare",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
| Bolt M8 | Grade `8.8` |
|
| Bolt M8 | Grade `8.8` |
|
||||||
| Nut \| washer | Sold as a pair |
|
| Nut \| washer | Sold as a pair |
|
||||||
| Washer M8 | 100 pcs:hardBreak{}zinc-plated |
|
| Washer M8 | 100 pcs:hardBreak{}zinc-plated |
|
||||||
| Spare | |
|
| :text{text=" "}Spare | |
|
||||||
|
|||||||
+2
-1
@@ -260,7 +260,8 @@ The moon, at night.
|
|||||||
**The CommonMark image.** A paragraph whose entire inline content is one image `` is
|
**The CommonMark image.** A paragraph whose entire inline content is one image `` is
|
||||||
a `mediaSingle` with attrs exactly `{"layout":"center"}` holding an `external` `media` — `url`
|
a `mediaSingle` with attrs exactly `{"layout":"center"}` holding an `external` `media` — `url`
|
||||||
from the destination, `alt` the description's plain-text content when non-empty — a link or image
|
from the destination, `alt` the description's plain-text content when non-empty — a link or image
|
||||||
inside it contributing its own text, and a break of either kind a space. `adfToMarkdown`
|
inside it contributing its own text, a node spelling its text in the content slot contributing
|
||||||
|
that text, and a break of either kind a space. `adfToMarkdown`
|
||||||
emits the image form for exactly that shape — those attrs and no others, no marks on either
|
emits the image form for exactly that shape — those attrs and no others, no marks on either
|
||||||
node, no caption, and a `media` carrying nothing beyond `alt`, `type` and `url` — and only where
|
node, no caption, and a `media` carrying nothing beyond `alt`, `type` and `url` — and only where
|
||||||
CommonMark spells the pair: a destination or a description the image form cannot hold, an empty
|
CommonMark spells the pair: a destination or a description the image form cannot hold, an empty
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ export function readInlineDirective(text: string, index: number): Read<Directive
|
|||||||
return readNestedDirective(text, index, 1)
|
return readNestedDirective(text, index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Both directions answer alike: an inline directive never spans lines, so no content slot holds a line ending.
|
||||||
|
export function slotLineEndingFault(type: string, text: string): ConvertFault | undefined {
|
||||||
|
if (!/[\n\r]/.test(text)) return undefined
|
||||||
|
return { code: 'unspellable-whitespace', message: `the ${type} content slot holds a newline no inline directive spans` }
|
||||||
|
}
|
||||||
|
|
||||||
export function spellAttributes(pairs: readonly (readonly [string, string])[]): string {
|
export function spellAttributes(pairs: readonly (readonly [string, string])[]): string {
|
||||||
if (pairs.length === 0) return ''
|
if (pairs.length === 0) return ''
|
||||||
const spelled = [...pairs].sort(([left], [right]) => keyOrder(left, right)).map(([key, value]) => `${key}=${value}`)
|
const spelled = [...pairs].sort(([left], [right]) => keyOrder(left, right)).map(([key, value]) => `${key}=${value}`)
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import type { InlineDirective } from '../../adf/inline-directives.ts'
|
|||||||
import { assembleInlineLine, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts'
|
import { assembleInlineLine, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts'
|
||||||
import { carriedInline } from '../opaque-carry.ts'
|
import { carriedInline } from '../opaque-carry.ts'
|
||||||
import { claimsLine, holdsNullCharacter, isAutolink } from '../commonmark-grammar.ts'
|
import { claimsLine, holdsNullCharacter, isAutolink } from '../commonmark-grammar.ts'
|
||||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { holdsEntityReference } from '../entity-references.ts'
|
import { holdsEntityReference } from '../entity-references.ts'
|
||||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { longestBacktickRun } from '../backtick-runs.ts'
|
import { longestBacktickRun } from '../backtick-runs.ts'
|
||||||
import { markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
import { markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
||||||
import { sameMark } from '../../adf/editor-normal.ts'
|
import { sameMark } from '../../adf/editor-normal.ts'
|
||||||
|
import { slotLineEndingFault, spellLeafDirective } from '../directive-syntax.ts'
|
||||||
import { spellDestination, spellTitle } from '../link-syntax.ts'
|
import { spellDestination, spellTitle } from '../link-syntax.ts'
|
||||||
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
|
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
|
||||||
import { spellLeafDirective } from '../directive-syntax.ts'
|
|
||||||
import { spellTextDirective } from '../text-directive.ts'
|
import { spellTextDirective } from '../text-directive.ts'
|
||||||
|
|
||||||
type EmittedLine = { line: string; segments: InlineSegment[] }
|
type EmittedLine = { line: string; segments: InlineSegment[] }
|
||||||
@@ -207,7 +207,8 @@ function emitInlineDirective(node: AdfNode, directive: InlineDirective, index: n
|
|||||||
const slot = directive.textAttribute === undefined ? undefined : node.attrs?.[directive.textAttribute]
|
const slot = directive.textAttribute === undefined ? undefined : node.attrs?.[directive.textAttribute]
|
||||||
if (slot === undefined) return success({ segments: [syntax(spellLeafDirective(node.type, attributes))] })
|
if (slot === undefined) return success({ segments: [syntax(spellLeafDirective(node.type, attributes))] })
|
||||||
if (typeof slot !== 'string') return success({ carry: { first: index, last: index } })
|
if (typeof slot !== 'string') return success({ carry: { first: index, last: index } })
|
||||||
if (/[\n\r]/.test(slot)) return failure('unspellable-whitespace', `a ${node.type} content slot holds a newline no inline directive spans`, path)
|
const spans = slotLineEndingFault(node.type, slot)
|
||||||
|
if (spans !== undefined) return faulted(spans, path)
|
||||||
if (holdsNullCharacter(slot)) return failure('unspellable-character', `a ${node.type} content slot holds a null character CommonMark replaces`, path)
|
if (holdsNullCharacter(slot)) return failure('unspellable-character', `a ${node.type} content slot holds a null character CommonMark replaces`, path)
|
||||||
const content: InlineSegment[] = slot === '' ? [] : [{ escaping: 'bracketed', text: slot }]
|
const content: InlineSegment[] = slot === '' ? [] : [{ escaping: 'bracketed', text: slot }]
|
||||||
return success({ segments: [syntax(`:${node.type}[`), ...content, syntax(`]${attributes}`)] })
|
return success({ segments: [syntax(`:${node.type}[`), ...content, syntax(`]${attributes}`)] })
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { failure, faulted, success, type ConvertErrorPath, type Result } from '.
|
|||||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||||
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
|
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
|
||||||
import { readVocabulary } from './directive-attributes.ts'
|
import { readVocabulary } from './directive-attributes.ts'
|
||||||
|
import { slotLineEndingFault } from '../directive-syntax.ts'
|
||||||
|
|
||||||
export type BlockDirectiveNode = { contentModel: BlockDirective['contentModel']; node: AdfNode }
|
export type BlockDirectiveNode = { contentModel: BlockDirective['contentModel']; node: AdfNode }
|
||||||
|
|
||||||
@@ -56,6 +57,8 @@ export function readInlineDirectiveNode(
|
|||||||
if (slot !== undefined && content !== undefined) {
|
if (slot !== undefined && content !== undefined) {
|
||||||
const text = slotText(content)
|
const text = slotText(content)
|
||||||
if (text === undefined) return failure('unsupported-node-shape', `the ${name} content slot holds one unmarked text node`, path)
|
if (text === undefined) return failure('unsupported-node-shape', `the ${name} content slot holds one unmarked text node`, path)
|
||||||
|
const spans = slotLineEndingFault(name, text)
|
||||||
|
if (spans !== undefined) return faulted(spans, path)
|
||||||
attrs.value[slot] = text
|
attrs.value[slot] = text
|
||||||
}
|
}
|
||||||
return success(namedNode(name, attrs.value, undefined))
|
return success(namedNode(name, attrs.value, undefined))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { backslashEscape, decodeTextEscapes, inlineHtmlConstruct, readBracketedA
|
|||||||
import { backtickRun, closingBacktickRun } from '../backtick-runs.ts'
|
import { backtickRun, closingBacktickRun } from '../backtick-runs.ts'
|
||||||
import { delimiterFlags, matchEmphasis, runLength } from '../emphasis-matching.ts'
|
import { delimiterFlags, matchEmphasis, runLength } from '../emphasis-matching.ts'
|
||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
|
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||||
import { mergeAdjacentText } from '../../adf/editor-normal.ts'
|
import { mergeAdjacentText } from '../../adf/editor-normal.ts'
|
||||||
import { normalizeLabel, readInlineTarget, readLabel } from '../link-syntax.ts'
|
import { normalizeLabel, readInlineTarget, readLabel } from '../link-syntax.ts'
|
||||||
import { readDirectiveMark } from './directive-marks.ts'
|
import { readDirectiveMark } from './directive-marks.ts'
|
||||||
@@ -305,9 +306,15 @@ function closeImage(scan: Scan, at: number, inner: readonly Piece[], definition:
|
|||||||
}
|
}
|
||||||
|
|
||||||
function imageAlt(inner: readonly Piece[]): string {
|
function imageAlt(inner: readonly Piece[]): string {
|
||||||
return resolveNodes(inner)
|
return resolveNodes(inner).map(altText).join('')
|
||||||
.map((node) => (node.type === 'hardBreak' ? ' ' : (node.text ?? '')))
|
}
|
||||||
.join('')
|
|
||||||
|
// spec/flavour.md, The CommonMark image: the description's plain text, the content slot included.
|
||||||
|
function altText(node: AdfNode): string {
|
||||||
|
if (node.type === 'hardBreak') return ' '
|
||||||
|
const slot = inlineDirective(node.type)?.textAttribute
|
||||||
|
const spelled = slot === undefined ? undefined : node.attrs?.[slot]
|
||||||
|
return typeof spelled === 'string' ? spelled : (node.text ?? '')
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveNodes(pieces: readonly Piece[]): AdfNode[] {
|
function resolveNodes(pieces: readonly Piece[]): AdfNode[] {
|
||||||
|
|||||||
@@ -647,6 +647,8 @@ test('flattens the description of a lone image to the plain text alt holds', ()
|
|||||||
assert.deepEqual(content(markdownToAdf('\n')), [image('/u', 'a b')])
|
assert.deepEqual(content(markdownToAdf('\n')), [image('/u', 'a b')])
|
||||||
assert.deepEqual(content(markdownToAdf('\n')), [image('/u', 'a b')])
|
assert.deepEqual(content(markdownToAdf('\n')), [image('/u', 'a b')])
|
||||||
assert.deepEqual(content(markdownToAdf('\n')), [image('/u', 'a b')])
|
assert.deepEqual(content(markdownToAdf('\n')), [image('/u', 'a b')])
|
||||||
|
assert.deepEqual(content(markdownToAdf('![a :mention[@A]{id=b1c2} b](/u)\n')), [image('/u', 'a @A b')])
|
||||||
|
assert.deepEqual(content(markdownToAdf('![:mention[@A]{id=b1c2}](/u)\n')), [image('/u', '@A')])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('leaves the brackets of an empty link text the text they are', () => {
|
test('leaves the brackets of an empty link text the text they are', () => {
|
||||||
@@ -697,9 +699,12 @@ test('names the content slot no one unmarked text node reads back from', () => {
|
|||||||
assert.equal(code(markdownToAdf(':status[:date{timestamp=1}]{color=yellow}\n')), 'unsupported-node-shape')
|
assert.equal(code(markdownToAdf(':status[:date{timestamp=1}]{color=yellow}\n')), 'unsupported-node-shape')
|
||||||
assert.equal(content(markdownToAdf(':status[]{color=yellow}\n')), 'unmappable-image: an image fits only as a paragraph of its own')
|
assert.equal(content(markdownToAdf(':status[]{color=yellow}\n')), 'unmappable-image: an image fits only as a paragraph of its own')
|
||||||
assert.equal(code(markdownToAdf(':status[<div>]{color=yellow}\n')), 'unmappable-html')
|
assert.equal(code(markdownToAdf(':status[<div>]{color=yellow}\n')), 'unmappable-html')
|
||||||
// The slot parses before the name's table is consulted, so a doubly-broken span reports its inner error.
|
|
||||||
assert.equal(code(markdownToAdf(':date[<div>]{timestamp=1}\n')), 'unmappable-html')
|
assert.equal(code(markdownToAdf(':date[<div>]{timestamp=1}\n')), 'unmappable-html')
|
||||||
assert.equal(code(markdownToAdf(':widget[<div>]\n')), 'unmappable-html')
|
assert.equal(code(markdownToAdf(':widget[<div>]\n')), 'unmappable-html')
|
||||||
|
const spans = 'unspellable-whitespace: the status content slot holds a newline no inline directive spans'
|
||||||
|
assert.equal(content(markdownToAdf(':status[:text{text="\\n"}]{color=yellow}\n')), spans)
|
||||||
|
assert.equal(content(markdownToAdf(':status[a b]{color=yellow}\n')), spans)
|
||||||
|
assert.equal(content(markdownToAdf(':status[a b]{color=yellow}\n')), spans)
|
||||||
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot')
|
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -733,6 +738,9 @@ test('reads the directive marks, the nesting outermost first', () => {
|
|||||||
assert.deepEqual(content(markdownToAdf(':underline[a:date{timestamp=1}]\n')), [
|
assert.deepEqual(content(markdownToAdf(':underline[a:date{timestamp=1}]\n')), [
|
||||||
{ content: [marked('a', underline), { attrs: { timestamp: '1' }, marks: [underline], type: 'date' }], type: 'paragraph' },
|
{ content: [marked('a', underline), { attrs: { timestamp: '1' }, marks: [underline], type: 'date' }], type: 'paragraph' },
|
||||||
])
|
])
|
||||||
|
assert.deepEqual(content(markdownToAdf('_:underline[a:date{timestamp=1}]_\n')), [
|
||||||
|
{ content: [marked('a', em, underline), { attrs: { timestamp: '1' }, marks: [em, underline], type: 'date' }], type: 'paragraph' },
|
||||||
|
])
|
||||||
assert.equal(content(markdownToAdf(':border[a]{color="#091e42" size=x}\n')), 'unsupported-node-shape: the size attribute of border is no number')
|
assert.equal(content(markdownToAdf(':border[a]{color="#091e42" size=x}\n')), 'unsupported-node-shape: the size attribute of border is no number')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -357,8 +357,13 @@ Under **3 — `markdownToAdf` (`0.1.0`)**:
|
|||||||
CommonMark carries plainly, are named errors, as 3g refuses the directive form of a node
|
CommonMark carries plainly, are named errors, as 3g refuses the directive form of a node
|
||||||
CommonMark spells. The reader takes the slot's parsed nodes rather than its text, so the
|
CommonMark spells. The reader takes the slot's parsed nodes rather than its text, so the
|
||||||
rule refusing anything but one unmarked text node sits beside the node tables that own the
|
rule refusing anything but one unmarked text node sits beside the node tables that own the
|
||||||
slot, and a node taking no content still names that first. `directive-content-slot` stays
|
slot. Only `text`, read ahead of the slot, names a refusal before the slot's own: a
|
||||||
with the fixtures, its cause now a marked slot rather than a slot at all.
|
doubly-broken span reports what its content holds, `:date[<div>]{timestamp=1}` being
|
||||||
|
`unmappable-html` rather than `date takes no content`, which the maintainer pinned with an
|
||||||
|
assertion rather than reordering the readers. `directive-content-slot` stays with the
|
||||||
|
fixtures, its cause now a marked slot rather than a slot at all. The slot's own whitespace
|
||||||
|
answers the rule the spelling does: `:text{text="\n"}` and ` ` alike reach a slot the
|
||||||
|
emitter refuses a line ending in, so one function answers both directions.
|
||||||
The same read found the hole the other way: `attemptLine` refused a line edged with a
|
The same read found the hole the other way: `attemptLine` refused a line edged with a
|
||||||
vertical tab or a form feed, where CommonMark strips spaces and tabs alone, so valid
|
vertical tab or a form feed, where CommonMark strips spaces and tabs alone, so valid
|
||||||
CommonMark parsed to a document `adfToMarkdown` then refused. The edges that check covered
|
CommonMark parsed to a document `adfToMarkdown` then refused. The edges that check covered
|
||||||
|
|||||||
Reference in New Issue
Block a user