mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer stability round 1: 0 🔴, 6 🟡 (0 misread); restore inlineRuns's early exit, one mark equality, refuse a pass taking no mark, pin the carried range and the opening link's run
This commit is contained in:
@@ -49,6 +49,16 @@ test('refuses a node whose carry nests too deep ahead of a later node in its lin
|
||||
assert.deepEqual(path(refused), ['content', 0, 'content', 0])
|
||||
})
|
||||
|
||||
test('refuses a carried run at the node whose carry nests too deep, among the run\'s own nodes', () => {
|
||||
let deep: JsonValue = 'a'
|
||||
for (let depth = 0; depth < largestNesting - 1; depth += 1) deep = { deep }
|
||||
const carriedEm: AdfNode = { marks: [{ attrs: { deep }, type: 'em' }], text: 'x', type: 'text' }
|
||||
const twiceStrong: AdfNode = { marks: [{ type: 'strong' }, { type: 'strong' }], text: 'y', type: 'text' }
|
||||
const refused = adfToLosslessMarkdown(document(paragraph({ text: 'ok', type: 'text' }, carriedEm, twiceStrong)))
|
||||
assert.equal(code(refused), 'unsupported-nesting-depth')
|
||||
assert.deepEqual(path(refused), ['content', 0, 'content', 1])
|
||||
})
|
||||
|
||||
test('refuses a value that is not an ADF document, naming the check it failed', () => {
|
||||
assert.equal(markdown(adfToLosslessMarkdown({ type: 'doc', version: Number.NaN })), 'not-an-adf-document: an ADF document holds a version number: found NaN')
|
||||
})
|
||||
|
||||
@@ -234,13 +234,15 @@ function inlineRuns(nodes: readonly RunNode[], firstIndex: number, carried: Read
|
||||
for (const [offset, runNode] of nodes.entries()) {
|
||||
const index = firstIndex + offset
|
||||
const { node, outer } = runNode
|
||||
const previous = runs[runs.length - 1]
|
||||
// A carried node spells its marks inside the carry.
|
||||
const carriedNode = carries(node, carried, index)
|
||||
const inside = carriedNode || previous?.kind !== 'marked' ? undefined : insideRun(runNode, previous.mark)
|
||||
const mark = carriedNode ? undefined : nodeMarks(node)[outer]
|
||||
const mark = carries(node, carried, index) ? undefined : nodeMarks(node)[outer]
|
||||
if (mark === undefined) {
|
||||
runs.push({ index, kind: 'plain', node })
|
||||
continue
|
||||
}
|
||||
const previous = runs[runs.length - 1]
|
||||
const inside = previous?.kind === 'marked' ? insideRun(runNode, previous.mark) : undefined
|
||||
if (previous?.kind === 'marked' && inside !== undefined) previous.nodes.push(inside)
|
||||
else if (mark === undefined) runs.push({ index, kind: 'plain', node })
|
||||
else runs.push({ index, kind: 'marked', mark, nodes: [{ node, outer: outer + 1 }] })
|
||||
}
|
||||
return runs
|
||||
|
||||
@@ -465,6 +465,11 @@ test('writes what the editor-normal form of the document writes', () => {
|
||||
assert.equal(portable(paragraph({ marks: [{ attrs: {}, type: 'code' }], text: 'a', type: 'text' }, { marks: [{ type: 'code' }], text: '|b', type: 'text' })), '`a|b`\n')
|
||||
})
|
||||
|
||||
test('keeps the code mark of a node past an opening link when the fallback drops the link\'s own', () => {
|
||||
const link: AdfMark = { attrs: { href: 'u' }, type: 'link' }
|
||||
assert.equal(portable(paragraph(text('x]: y', link, code), text('z', code))), '[x\\]: y](u)`z`\n')
|
||||
})
|
||||
|
||||
test('writes a text node holding content the same in either order beside its neighbour', () => {
|
||||
const holding: AdfNode = { content: [text('inner')], text: 'a', type: 'text' }
|
||||
assert.equal(portable(paragraph(holding, text('b'))), 'ab\n')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { JsonValue } from '../../json-value.ts'
|
||||
import { identicalMark, mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { joinsWhenRead } from '../adjacent-text.ts'
|
||||
|
||||
type JsonContainer = JsonValue[] | { [key: string]: JsonValue }
|
||||
@@ -13,10 +13,6 @@ export function joinsWhenEditorNormal(previous: AdfNode, node: AdfNode): boolean
|
||||
return joinsWhenRead(normalNode(previous), normalNode(node))
|
||||
}
|
||||
|
||||
export function sameMarkWhenEditorNormal(left: AdfMark, right: AdfMark): boolean {
|
||||
return identicalMark(normalMark(left), normalMark(right))
|
||||
}
|
||||
|
||||
export function toEditorNormal(document: AdfDocument): AdfDocument {
|
||||
const normal: AdfDocument = { type: document.type, version: Object.is(document.version, -0) ? 0 : document.version }
|
||||
const pending: { holder: NodeHolder; source: NodeHolder }[] = [{ holder: normal, source: document }]
|
||||
|
||||
@@ -37,6 +37,30 @@ test('reduces a line alike dropping every run a pass may or the first alone', ()
|
||||
})
|
||||
|
||||
// Drops only the first run each pass finds, with every misflanked highlight, and takes no cap.
|
||||
test('refuses an opening-link fallback whose link run holds no code span', () => {
|
||||
const link = { attrs: { href: 'u' }, type: 'link' }
|
||||
const lines: AdfNode[][] = [
|
||||
[{ marks: [link], text: 'a', type: 'text' }],
|
||||
[{ marks: [link, { type: 'em' }], text: 'a', type: 'text' }],
|
||||
[{ marks: [link], text: 'a', type: 'text' }, { marks: [{ type: 'code' }], text: 'c', type: 'text' }],
|
||||
]
|
||||
for (const line of lines) {
|
||||
const refused = withoutFallback(line, { kind: 'opening-link' }, [])
|
||||
assert.equal(refused.ok ? 'kept' : refused.error.code, 'unsupported-node-shape')
|
||||
}
|
||||
})
|
||||
|
||||
test('takes code off every node of an opening link\'s run that an opening-link fallback names', () => {
|
||||
const link = { attrs: { href: 'u' }, type: 'link' }
|
||||
const line: AdfNode[] = [{ marks: [link, { type: 'code' }], text: 'a', type: 'text' }, { marks: [link], text: 'b', type: 'text' }]
|
||||
assert.deepEqual(withoutFallback(line, { kind: 'opening-link' }, []), success([{ marks: [link], text: 'a', type: 'text' }, { marks: [link], text: 'b', type: 'text' }]))
|
||||
})
|
||||
|
||||
test('refuses an unspellable-run fallback that takes no mark', () => {
|
||||
const refused = withoutFallback([{ marks: [{ type: 'em' }], text: 'a', type: 'text' }], { kind: 'unspellable-run', runs: [{ depth: 1, first: 0, last: 0 }] }, [])
|
||||
assert.equal(refused.ok ? 'kept' : refused.error.code, 'unsupported-node-shape')
|
||||
})
|
||||
|
||||
function firstRunLine(nodes: readonly AdfNode[], container: LineContainer): Result<AdfNode[]> {
|
||||
const leaves = portableLeaves(nodes, container, [], 0)
|
||||
if (!leaves.ok) return leaves
|
||||
|
||||
@@ -3,11 +3,11 @@ import type { LineContainer } from '../line-container.ts'
|
||||
import type { MarkRun } from '../emit/line-escaping.ts'
|
||||
import { codeTextMarkTypes } from '../../adf/inline-nodes.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { identicalMark, mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { imageAside } from '../image-aside.ts'
|
||||
import { insideRun, isEmphasis, type RunNode } from '../mark-spellings.ts'
|
||||
import { joinsWhenEditorNormal, sameMarkWhenEditorNormal } from './editor-normal.ts'
|
||||
import { joinsWhenEditorNormal } from './editor-normal.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../../adf/shown-text.ts'
|
||||
import { portableLineFallback, readBackPasses, type PortableLineFallback } from '../emit/inline-line.ts'
|
||||
import { spellDestination } from '../commonmark/link-syntax.ts'
|
||||
@@ -145,11 +145,11 @@ function highlightsInsideSharedMarks(leaves: readonly AdfNode[]): AdfNode[] {
|
||||
const marks = nodeMarks(leaf)
|
||||
if (marks[0]?.type === highlight) {
|
||||
const held = marks.slice(1)
|
||||
shared = run.length === 0 ? held : shared.filter((mark) => held.some((other) => sameMarkWhenEditorNormal(other, mark)))
|
||||
shared = run.length === 0 ? held : shared.filter((mark) => held.some((other) => identicalMark(other, mark)))
|
||||
run.push(leaf)
|
||||
continue
|
||||
}
|
||||
for (const held of run) spelled.push(withMarks(held, [...shared, highlightMark, ...nodeMarks(held).slice(1).filter((mark) => !shared.some((other) => sameMarkWhenEditorNormal(other, mark)))]))
|
||||
for (const held of run) spelled.push(withMarks(held, [...shared, highlightMark, ...nodeMarks(held).slice(1).filter((mark) => !shared.some((other) => identicalMark(other, mark)))]))
|
||||
run = []
|
||||
spelled.push(leaf)
|
||||
}
|
||||
@@ -229,7 +229,7 @@ function sharedRuns(marks: readonly AdfMark[], neighbour: AdfNode): number {
|
||||
}
|
||||
|
||||
function spellableLine(leaves: AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<AdfNode[]> {
|
||||
// Terminates: each fallback takes a mark off some leaf, and trimmedEdges adds none to any character.
|
||||
// Terminates: withoutFallback refuses a pass that takes no mark, and trimmedEdges adds none to any character.
|
||||
for (let current = leaves, pass = 1; ; pass += 1) {
|
||||
const fallback = portableLineFallback(current, container, path)
|
||||
if (!fallback.ok) return fallback
|
||||
@@ -260,7 +260,7 @@ function withoutEmphasis(leaves: readonly AdfNode[]): AdfNode[] {
|
||||
}
|
||||
|
||||
// The runs one pass takes back may nest, so a leaf drops its mark at each depth a run over it names.
|
||||
function withoutMarks(leaves: readonly AdfNode[], runs: readonly MarkRun[]): AdfNode[] {
|
||||
function withoutMarks(leaves: readonly AdfNode[], runs: readonly MarkRun[]): AdfNode[] | undefined {
|
||||
const depths = new Map<number, number[]>()
|
||||
for (const run of runs) {
|
||||
for (let index = run.first; index <= run.last; index += 1) {
|
||||
@@ -269,8 +269,13 @@ function withoutMarks(leaves: readonly AdfNode[], runs: readonly MarkRun[]): Adf
|
||||
else held.push(run.depth)
|
||||
}
|
||||
}
|
||||
return leaves.map((leaf, index) => {
|
||||
const kept = leaves.map((leaf, index) => {
|
||||
const dropped = depths.get(index)
|
||||
return dropped === undefined ? leaf : withMarks(leaf, nodeMarks(leaf).filter((_, held) => !dropped.includes(held)))
|
||||
})
|
||||
return markCount(kept) < markCount(leaves) ? kept : undefined
|
||||
}
|
||||
|
||||
function markCount(leaves: readonly AdfNode[]): number {
|
||||
return leaves.reduce((count, leaf) => count + nodeMarks(leaf).length, 0)
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ mention or code spelled `!adf:text{text=" "}`. Joining the run across the break
|
||||
`inline-line.ts` ruled worse with two comprehension panels (PR #22): a joined run that cannot be spelled needs its own fallback,
|
||||
first as a set of parted breaks, then as a flag the first fallback clears.
|
||||
|
||||
Once a run spans a node that doesn't hold its mark, `outer` and `depth` differ. Settle which one `tryLink`'s `depth + 1` and `MarkRun.depth` mean, and keep `MarkRun.depth`'s comment true.
|
||||
|
||||
### 133. Write mentions, emoji, dates, statuses and inline cards as semantic HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.
|
||||
|
||||
A writer panel picks the elements, classes and `data-*` attributes. Where a link marks a card, both
|
||||
|
||||
Reference in New Issue
Block a user