Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45f1bee94b | |||
| 20a191e7e9 |
+3
-2
@@ -40,8 +40,9 @@ shape the editor would not.
|
|||||||
empty `attrs`, `content` or `marks` and `-0` included, with no normalization on either side.
|
empty `attrs`, `content` or `marks` and `-0` included, with no normalization on either side.
|
||||||
CommonMark's spelling stays wherever a document holds none of those shapes. The plain reader
|
CommonMark's spelling stays wherever a document holds none of those shapes. The plain reader
|
||||||
builds what is written, as `markdownToAdf` does. Only the plain writer is lossy: its reduction
|
builds what is written, as `markdownToAdf` does. Only the plain writer is lossy: its reduction
|
||||||
writes editor-normal ADF — adjacent text nodes of identical marks and no attributes merged, `-0`
|
reads and writes editor-normal ADF — adjacent text nodes of identical marks and no attributes
|
||||||
as `0`, and an empty `attrs`, `content` or `marks` the absent key.
|
merged, `-0` as `0`, and an empty `attrs`, `content` or `marks` the absent key — so two documents
|
||||||
|
the editor holds equal write the same plain markdown.
|
||||||
|
|
||||||
## `!adf:textBreak{}` parts text CommonMark would join
|
## `!adf:textBreak{}` parts text CommonMark would join
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { adfToMarkdown } from '../markdown/emit/adf-to-markdown.ts'
|
|||||||
import { adfToPlainMarkdown, reduceToPlain } from '../markdown/emit/plain-reduction.ts'
|
import { adfToPlainMarkdown, reduceToPlain } from '../markdown/emit/plain-reduction.ts'
|
||||||
import { directivePrefix } from '../markdown/directive-syntax.ts'
|
import { directivePrefix } from '../markdown/directive-syntax.ts'
|
||||||
import { markdownToAdf, plainMarkdownToAdf } from '../markdown/parse/markdown-to-adf.ts'
|
import { markdownToAdf, plainMarkdownToAdf } from '../markdown/parse/markdown-to-adf.ts'
|
||||||
|
import { toEditorNormal } from '../adf/editor-normal.ts'
|
||||||
|
|
||||||
const gateRuns = 1600
|
const gateRuns = 1600
|
||||||
const renamedPrefix = '!adg:'
|
const renamedPrefix = '!adg:'
|
||||||
@@ -61,3 +62,12 @@ test('a generated document writes plain markdown refusing only what the guard re
|
|||||||
propertyRuns(gateRuns),
|
propertyRuns(gateRuns),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('a generated document writes the plain markdown its editor-normal form writes', { timeout: propertyTimeout }, () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(adfDocument, (document) => {
|
||||||
|
assert.deepEqual(adfToPlainMarkdown(document), adfToPlainMarkdown(toEditorNormal(document)))
|
||||||
|
}),
|
||||||
|
propertyRuns(gateRuns),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
@@ -338,3 +338,9 @@ test('keeps the nodes the plain flavour spells and degrades only what it cannot'
|
|||||||
const reduced = reduceToPlain(document(node('panel', { localId: '01a0d99b-1f56-7a50-889a-f4375f09ee05', panelType: 'info' }, said('x')), tasks))
|
const reduced = reduceToPlain(document(node('panel', { localId: '01a0d99b-1f56-7a50-889a-f4375f09ee05', panelType: 'info' }, said('x')), tasks))
|
||||||
assert.deepEqual(reduced.ok ? reduced.value : undefined, document(node('panel', { panelType: 'info' }, said('x')), { content: [node('taskItem', { state: 'DONE' }, text('t'))], type: 'taskList' }))
|
assert.deepEqual(reduced.ok ? reduced.value : undefined, document(node('panel', { panelType: 'info' }, said('x')), { content: [node('taskItem', { state: 'DONE' }, text('t'))], type: 'taskList' }))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('writes what the editor-normal form of the document writes', () => {
|
||||||
|
assert.equal(plain({ attrs: { order: -0 }, content: [{ content: [], type: 'listItem' }], type: 'orderedList' }), '0.\n')
|
||||||
|
assert.equal(plain({ content: [], text: 'hi', type: 'futureInline' }), 'hi\n')
|
||||||
|
assert.equal(plain(paragraph({ marks: [{ attrs: {}, type: 'code' }], text: 'a', type: 'text' }, { marks: [{ type: 'code' }], text: '|b', type: 'text' })), '`a|b`\n')
|
||||||
|
})
|
||||||
|
|||||||
@@ -51,9 +51,14 @@ export function reduceToPlain(document: AdfDocument): Result<AdfDocument> {
|
|||||||
const fault = adfDocumentFault(document)
|
const fault = adfDocumentFault(document)
|
||||||
if (fault !== undefined) return faulted(fault, [])
|
if (fault !== undefined) return faulted(fault, [])
|
||||||
if (document.version !== 1) return failure('unsupported-document-version', `no markdown spelling carries ADF version ${document.version}`, [])
|
if (document.version !== 1) return failure('unsupported-document-version', `no markdown spelling carries ADF version ${document.version}`, [])
|
||||||
// A refusal's path indexes the caller's document, so only the reduction's output normalizes (docs/decisions.md §Equality is deep).
|
// docs/decisions.md §Equality is deep: the reduction reads and writes editor-normal ADF.
|
||||||
const blocks = reduceBlocks(nodeContent(document), { depth: 0, memo: new Map(), path: [] })
|
const blocks = reduceBlocks(nodeContent(toEditorNormal(document)), { depth: 0, memo: new Map(), path: [] })
|
||||||
return blocks.ok ? success({ content: nodeContent(toEditorNormal({ content: blocks.value, type: 'doc', version: 1 })).slice(), type: 'doc', version: 1 }) : blocks
|
if (!blocks.ok) {
|
||||||
|
// Merging text renumbers siblings, so the caller's document names the refusal's path.
|
||||||
|
const raw = reduceBlocks(nodeContent(document), { depth: 0, memo: new Map(), path: [] })
|
||||||
|
return raw.ok ? blocks : raw
|
||||||
|
}
|
||||||
|
return success({ content: nodeContent(toEditorNormal({ content: blocks.value, type: 'doc', version: 1 })).slice(), type: 'doc', version: 1 })
|
||||||
}
|
}
|
||||||
|
|
||||||
function reduceBlocks(nodes: readonly AdfNode[], reduction: Reduction): Result<AdfNode[]> {
|
function reduceBlocks(nodes: readonly AdfNode[], reduction: Reduction): Result<AdfNode[]> {
|
||||||
@@ -86,7 +91,7 @@ function reduceStanding(node: AdfNode, reduction: Reduction): Result<AdfNode[]>
|
|||||||
|
|
||||||
function standsInline(node: AdfNode): boolean {
|
function standsInline(node: AdfNode): boolean {
|
||||||
if (node.type === 'text' || inlineNodeModel(node.type) !== undefined) return true
|
if (node.type === 'text' || inlineNodeModel(node.type) !== undefined) return true
|
||||||
return !isBlockNodeType(node.type) && node.content === undefined
|
return !isBlockNodeType(node.type) && nodeContent(node).length === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function reduceBody(node: AdfNode, reduction: Reduction): Result<AdfNode[]> {
|
function reduceBody(node: AdfNode, reduction: Reduction): Result<AdfNode[]> {
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ type Scan = {
|
|||||||
spans: NestedSpans
|
spans: NestedSpans
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What a directive's content slot inherits from the content holding it.
|
||||||
|
type SharedScan = Pick<Scan, 'definitions' | 'ownMarks' | 'path'>
|
||||||
|
|
||||||
type SlotContent = { carry: boolean; nodes: Inline[] }
|
type SlotContent = { carry: boolean; nodes: Inline[] }
|
||||||
|
|
||||||
const carriedInMark = 'no mark spelling wraps an opaque carry or an inline node spelling marks=empty: its marks are its own'
|
const carriedInMark = 'no mark spelling wraps an opaque carry or an inline node spelling marks=empty: its marks are its own'
|
||||||
@@ -79,7 +82,7 @@ const textBreak: TextBreak = { kind: 'textBreak' }
|
|||||||
|
|
||||||
// The outermost content: only here does a text break see both its neighbours.
|
// The outermost content: only here does a text break see both its neighbours.
|
||||||
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, flavour: Flavour): Result<InlineContent> {
|
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, flavour: Flavour): Result<InlineContent> {
|
||||||
const scan: Scan = { container, deactivatedBefore: 0, definitions, highlights: flavour === 'plain', openingSpellableLink: false, ownMarks: new Set(), path, pending: '', pieces: [], source, spans: noSpans }
|
const scan = freshScan(source, { definitions, ownMarks: new Set(), path }, { container, highlights: flavour === 'plain', spans: noSpans })
|
||||||
const scanned = scanInline(scan)
|
const scanned = scanInline(scan)
|
||||||
if (!scanned.ok) return scanned
|
if (!scanned.ok) return scanned
|
||||||
if (scanned.value.image !== undefined) return success({ image: scanned.value.image })
|
if (scanned.value.image !== undefined) return success({ image: scanned.value.image })
|
||||||
@@ -93,6 +96,10 @@ export function parseInlineContent(source: string, definitions: LinkDefinitions,
|
|||||||
return success({ carry: scanned.value.carry, nodes: nodes.value })
|
return success({ carry: scanned.value.carry, nodes: nodes.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function freshScan(source: string, shared: SharedScan, own: Pick<Scan, 'container' | 'highlights' | 'spans'>): Scan {
|
||||||
|
return { ...shared, ...own, deactivatedBefore: 0, openingSpellableLink: false, pending: '', pieces: [], source }
|
||||||
|
}
|
||||||
|
|
||||||
function scanInline(scan: Scan): Result<Scanned> {
|
function scanInline(scan: Scan): Result<Scanned> {
|
||||||
const { source } = scan
|
const { source } = scan
|
||||||
let index = 0
|
let index = 0
|
||||||
@@ -274,7 +281,8 @@ function refuseLinkDirective(scan: Scan, mark: AdfMark, nodes: readonly Inline[]
|
|||||||
|
|
||||||
function slotContent(scan: Scan, span: DirectiveSpan): Result<SlotContent | undefined> {
|
function slotContent(scan: Scan, span: DirectiveSpan): Result<SlotContent | undefined> {
|
||||||
if (span.content === undefined) return success(undefined)
|
if (span.content === undefined) return success(undefined)
|
||||||
const parsed = scanInline({ ...scan, container: undefined, deactivatedBefore: 0, highlights: false, openingSpellableLink: false, pending: '', pieces: [], source: span.content, spans: span.spans })
|
const { definitions, ownMarks, path } = scan
|
||||||
|
const parsed = scanInline(freshScan(span.content, { definitions, ownMarks, path }, { container: undefined, highlights: false, spans: span.spans }))
|
||||||
if (!parsed.ok) return parsed
|
if (!parsed.ok) return parsed
|
||||||
if (parsed.value.image !== undefined) return failure('unmappable-image', imageAlone, scan.path)
|
if (parsed.value.image !== undefined) return failure('unmappable-image', imageAlone, scan.path)
|
||||||
return success(parsed.value)
|
return success(parsed.value)
|
||||||
|
|||||||
Reference in New Issue
Block a user