10c - adfToPlainMarkdown and plainMarkdownToAdf exported, whitespace-only leaves trimmed at a line end
This commit is contained in:
@@ -4,10 +4,21 @@ import test from 'node:test'
|
||||
|
||||
import { adfDocument, propertyRuns, propertyTimeout } from './property-harness.ts'
|
||||
import { adfToMarkdown } from '../markdown/emit/adf-to-markdown.ts'
|
||||
import { adfToPlainMarkdown } from '../markdown/emit/plain-reduction.ts'
|
||||
import { directivePrefix } from '../markdown/directive-syntax.ts'
|
||||
import { markdownToAdf } from '../markdown/parse/markdown-to-adf.ts'
|
||||
import { plainMarkdownToAdf } from '../markdown/parse/plain-lift.ts'
|
||||
import { toEditorNormal } from '../adf/editor-normal.ts'
|
||||
|
||||
const gateRuns = 1600
|
||||
const renamedPrefix = '!adg:'
|
||||
|
||||
// Renaming the prefix changes what markdown reads only where a directive was read.
|
||||
function readsNoDirective(markdown: string): boolean {
|
||||
const read = markdownToAdf(markdown)
|
||||
const renamed = markdownToAdf(markdown.replaceAll(directivePrefix, renamedPrefix))
|
||||
return read.ok && renamed.ok && JSON.stringify(read.value).replaceAll(directivePrefix, renamedPrefix) === JSON.stringify(renamed.value)
|
||||
}
|
||||
|
||||
test('a generated document refuses to emit, or its markdown reads back to it', { timeout: propertyTimeout }, () => {
|
||||
fc.assert(
|
||||
@@ -21,3 +32,17 @@ test('a generated document refuses to emit, or its markdown reads back to it', {
|
||||
propertyRuns(gateRuns),
|
||||
)
|
||||
})
|
||||
|
||||
test('a generated document writes plain markdown refusing only what the guard refuses, and that markdown reads back to itself', { timeout: propertyTimeout }, () => {
|
||||
fc.assert(
|
||||
fc.property(adfDocument, (document) => {
|
||||
const written = adfToPlainMarkdown(document)
|
||||
assert.ok(written.ok, written.ok ? '' : `${written.error.code}: ${written.error.message}`)
|
||||
assert.ok(readsNoDirective(written.value), `a directive in ${JSON.stringify(written.value)}`)
|
||||
const read = plainMarkdownToAdf(written.value)
|
||||
assert.ok(read.ok, read.ok ? '' : `${read.error.code}: ${read.error.message} — reading ${JSON.stringify(written.value)}`)
|
||||
assert.deepEqual(adfToPlainMarkdown(read.value), written, `reading ${JSON.stringify(written.value)}`)
|
||||
}),
|
||||
propertyRuns(gateRuns),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,5 +2,7 @@ export type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './adf/documen
|
||||
export type { ConvertError, ConvertErrorCode, ConvertErrorPath, ParseError, Result, SourcePosition } from './result.ts'
|
||||
export type { JsonValue } from './json-value.ts'
|
||||
export { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts'
|
||||
export { adfToPlainMarkdown } from './markdown/emit/plain-reduction.ts'
|
||||
export { isAdfDocument } from './adf/document.ts'
|
||||
export { markdownToAdf } from './markdown/parse/markdown-to-adf.ts'
|
||||
export { plainMarkdownToAdf } from './markdown/parse/plain-lift.ts'
|
||||
|
||||
@@ -221,10 +221,11 @@ function leafEdges(leaf: AdfNode, previous: AdfNode | undefined, next: AdfNode |
|
||||
const text = leaf.text
|
||||
if (text === undefined || marks.some((mark) => mark.type === 'code')) return undefined
|
||||
const lead = text.slice(0, text.search(/[^ \t]|$/))
|
||||
const trail = lead === text ? '' : text.slice(text.search(/[ \t]*$/))
|
||||
const trail = text.slice(Math.max(lead.length, text.search(/[ \t]*$/)))
|
||||
const leadDepth = edgeDepth(marks, previous, lead)
|
||||
const trailDepth = edgeDepth(marks, next, trail)
|
||||
const trailDepth = edgeDepth(marks, next, lead === text ? text : trail)
|
||||
if (leadDepth === marks.length && trailDepth === marks.length) return undefined
|
||||
if (lead === text) return leadDepth === undefined || trailDepth === undefined ? [] : [textLeaf(text, marks.slice(0, Math.min(leadDepth, trailDepth)))]
|
||||
const edges: AdfNode[] = []
|
||||
if (lead !== '' && leadDepth !== undefined) edges.push(textLeaf(lead, marks.slice(0, leadDepth)))
|
||||
const core = text.slice(lead.length, text.length - trail.length)
|
||||
|
||||
@@ -229,6 +229,7 @@ test('spells a node no row names, or one standing where no spelling holds it, as
|
||||
assert.equal(plain(bulletList(said('stray'), item(said('b')), text('loose'))), '- stray\n- b\n- loose\n')
|
||||
assert.equal(plain(bulletList()), '')
|
||||
assert.equal(plain(bulletList(item({ content: [text('a\n \nb')], type: 'codeBlock' }))), '- ```\n a\n\n b\n ```\n')
|
||||
assert.equal(plain(bulletList(item({ content: [text(' ')], type: 'codeBlock' }))), '- ```\n ```\n')
|
||||
assert.equal(plain(bulletList(item(node('rule', {}), node('rule', {}), said('Install')), item(said('Configure')))), '- Install\n- Configure\n')
|
||||
assert.equal(plain(bulletList(item(node('rule', {}), node('rule', {})), item(said('Configure')))), '-\n- Configure\n')
|
||||
assert.equal(plain(bulletList(item(bulletList(item(bulletList(item())))))), '- -\n')
|
||||
@@ -285,6 +286,7 @@ test('breaks a line at a newline and trims whitespace at every edge CommonMark s
|
||||
assert.equal(plain(paragraph(text(' \n a \n b\n'))), 'a\\\nb\n')
|
||||
assert.equal(plain(paragraph({ type: 'hardBreak' }, text('a'), { attrs: { text: '\n' }, type: 'hardBreak' }, text('b'), { type: 'hardBreak' })), 'a\\\nb\n')
|
||||
assert.equal(plain(paragraph(text('a'), text(' b ', strong), text('c'))), 'a **b** c\n')
|
||||
assert.equal(plain(paragraph(text(' '), text('b', strong), text(' \n'), text('c', strong), text(' '))), '**b**\\\n**c**\n')
|
||||
assert.equal(plain(paragraph(text('a'), text(' b ', em, strong), text(' ', em), text('c', em))), 'a _**b** c_\n')
|
||||
assert.equal(plain(paragraph(text(' x ', code))), '` x `\n')
|
||||
assert.equal(plain(node('heading', { level: 1 }, text(' h\ni '))), '# h i\n')
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||
import { adfDocumentFault, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||
import { alertMarker, foldedAlertMarker, taskMarker } from '../plain-conventions.ts'
|
||||
import { blockNodeModel } from '../../adf/block-nodes.ts'
|
||||
import { commonMarkSpelling, largestListMarker, type SpellingMemo } from './adf-to-markdown.ts'
|
||||
import { adfToMarkdown, commonMarkSpelling, largestListMarker, type SpellingMemo } from './adf-to-markdown.ts'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { inlineLeaves, isBlockNodeType, oneLine, reduceInline, writableHref } from './plain-inline.ts'
|
||||
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||
@@ -41,6 +41,11 @@ const blockReducers: Readonly<Record<string, BlockReducer>> = {
|
||||
taskList: reduceTaskList,
|
||||
}
|
||||
|
||||
export function adfToPlainMarkdown(document: AdfDocument): Result<string> {
|
||||
const reduced = reduceToPlain(document)
|
||||
return reduced.ok ? adfToMarkdown(reduced.value) : reduced
|
||||
}
|
||||
|
||||
export function reduceToPlain(document: AdfDocument): Result<AdfDocument> {
|
||||
const fault = adfDocumentFault(document)
|
||||
if (fault !== undefined) return faulted(fault, [])
|
||||
@@ -224,12 +229,13 @@ function listItem(blocks: Result<AdfNode[]>): Result<AdfNode[]> {
|
||||
// A list item's first line reads as no rule and holds no line of spaces alone: the rule and the spaces give way.
|
||||
function itemOf(blocks: readonly AdfNode[]): AdfNode {
|
||||
const rules = blocks.findIndex((block) => block.type !== 'rule')
|
||||
const content = blocks.slice(rules === -1 ? blocks.length : rules).map((block) => (block.type === 'codeBlock' ? { ...block, content: nodeContent(block).map(blankedLines) } : block))
|
||||
const content = blocks.slice(rules === -1 ? blocks.length : rules).map((block) => (block.type === 'codeBlock' ? { ...block, content: blankedLines(nodeContent(block)) } : block))
|
||||
return { content, type: 'listItem' }
|
||||
}
|
||||
|
||||
function blankedLines(code: AdfNode): AdfNode {
|
||||
return code.text === undefined ? code : { ...code, text: code.text.replace(/^[ \t]+$/gm, '') }
|
||||
function blankedLines(code: readonly AdfNode[]): AdfNode[] {
|
||||
const blanked = code.map((leaf) => leaf.text ?? '').join('').replace(/^[ \t]+$/gm, '')
|
||||
return blanked === '' ? [] : [text(blanked)]
|
||||
}
|
||||
|
||||
function reduceTaskList(node: AdfNode, reduction: Reduction): Result<AdfNode[]> {
|
||||
|
||||
@@ -2,8 +2,10 @@ import type { AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import { blockNodeModel } from '../../adf/block-nodes.ts'
|
||||
import { isWordCharacter } from '../commonmark/emphasis-matching.ts'
|
||||
import { highlightDelimiter, readAlertMarker, readTaskMarker } from '../plain-conventions.ts'
|
||||
import { markdownToAdf } from './markdown-to-adf.ts'
|
||||
import { mergeAdjacentText, sameMarks } from '../../adf/editor-normal.ts'
|
||||
import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { success, type ParseError, type Result } from '../../result.ts'
|
||||
|
||||
type Delimiter = { closes: boolean; holder: AdfNode; line: number; node: number; offset: number; opens: boolean; position: number }
|
||||
|
||||
@@ -11,6 +13,11 @@ type MarkerLed<T> = { marker: T; rest: AdfNode[] }
|
||||
|
||||
const editorHighlight: AdfMark = { attrs: { color: '#f8e6a0' }, type: 'backgroundColor' }
|
||||
|
||||
export function plainMarkdownToAdf(markdown: string): Result<AdfDocument, ParseError> {
|
||||
const parsed = markdownToAdf(markdown)
|
||||
return parsed.ok ? success(liftFromPlain(parsed.value)) : parsed
|
||||
}
|
||||
|
||||
export function liftFromPlain(document: AdfDocument): AdfDocument {
|
||||
return { ...document, content: liftBlocks(nodeContent(document), false) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user