mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-13 04:08:29 +01:00
Report a refusal's path where the node stands once the task list moves it
This commit is contained in:
@@ -8,7 +8,7 @@ import type { LinkDefinitions } from './inline-content.ts'
|
||||
import { carryFenceType, readCarriedBlock } from '../opaque-carry.ts'
|
||||
import { commonMarkSpelling, type SpellingMemo } from '../emit/adf-to-markdown.ts'
|
||||
import { documentAttribute, documentName, documentSpelling } from '../block-directive.ts'
|
||||
import { emptyKeys, nodeAttrs } from '../../adf/document.ts'
|
||||
import { emptyKeys, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result, type SourcePosition } from '../../result.ts'
|
||||
import { flavourClaims, leadingMarker, readAlertMarker, readTaskMarker } from '../portable/conventions.ts'
|
||||
import { languageSlot } from '../code-language.ts'
|
||||
@@ -19,7 +19,7 @@ import { parseInlineContent } from './inline-content.ts'
|
||||
import { readBlockDirectiveNode } from './directive-nodes.ts'
|
||||
import { spellStringAttribute, unsupportedNodeShape } from '../directive-syntax.ts'
|
||||
import { spellsEmpty } from '../empty-keys.ts'
|
||||
import { splitTaskList } from '../portable/task-list.ts'
|
||||
import { splitPlace, splitTaskList } from '../portable/task-list.ts'
|
||||
|
||||
type Paragraph = Extract<Block, { kind: 'paragraph' }>
|
||||
|
||||
@@ -68,7 +68,7 @@ function typeName(value: unknown): string {
|
||||
}
|
||||
|
||||
function readBlocks(blocks: readonly ReadBlock[], reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
||||
if (depth > largestNesting) return failure('unsupported-nesting-depth', `the input nests deeper than the ${largestNesting} levels the parser carries`, path)
|
||||
if (depth > largestNesting) return tooDeep(path)
|
||||
const content: AdfNode[] = []
|
||||
for (const block of blocks) {
|
||||
const nodePath = [...path, 'content', content.length]
|
||||
@@ -89,6 +89,10 @@ function readBlocks(blocks: readonly ReadBlock[], reading: Reading, path: Conver
|
||||
return success(content)
|
||||
}
|
||||
|
||||
function tooDeep<T>(path: ConvertErrorPath): Result<T> {
|
||||
return failure('unsupported-nesting-depth', `the input nests deeper than the ${largestNesting} levels the parser carries`, path)
|
||||
}
|
||||
|
||||
function documentFault(block: DirectiveBlock): ConvertFault | undefined {
|
||||
const spelled = block.attributes.get(documentAttribute.key)
|
||||
if (block.argument === undefined && block.attributes.size === 1 && spelled?.spelling === documentAttribute.value) return undefined
|
||||
@@ -186,27 +190,65 @@ function filledNode(node: AdfNode, content: Result<AdfNode[]>): Result<AdfNode>
|
||||
return success({ ...node, content: content.value.length === 0 ? [{ type: 'paragraph' }] : content.value })
|
||||
}
|
||||
|
||||
function bulletBlocks(items: readonly Block[][], reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
||||
function taskItems(items: readonly Block[][]) {
|
||||
const led = []
|
||||
for (const [first, ...others] of items) {
|
||||
const marked = markerLed(first, readTaskMarker)
|
||||
if (marked === undefined) {
|
||||
const list = listNode({ type: 'bulletList' }, items, reading, path, depth)
|
||||
return list.ok ? success([list.value]) : list
|
||||
}
|
||||
if (marked === undefined) return undefined
|
||||
led.push({ ...marked, others })
|
||||
}
|
||||
return led
|
||||
}
|
||||
|
||||
function bulletBlocks(items: readonly Block[][], reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
||||
const led = taskItems(items)
|
||||
if (led === undefined) {
|
||||
const list = listNode({ type: 'bulletList' }, items, reading, path, depth)
|
||||
return list.ok ? success([list.value]) : list
|
||||
}
|
||||
if (depth + 1 > largestNesting) return tooDeep([...path, 'content', 0])
|
||||
const tasks: AdfNode[] = []
|
||||
for (const [index, { marker, others, position, text }] of led.entries()) {
|
||||
const body = text.replace(/^(?:[ \t\n]|\\\n)+/, '')
|
||||
const marked: TaskParagraph[] = body === '' ? [] : [{ kind: 'taskParagraph', position, text: body }]
|
||||
const read = readBlocks([...marked, ...others], reading, [...path, 'content', index], depth + 1)
|
||||
if (!read.ok) return read
|
||||
tasks.push({ attrs: { state: marker.state }, content: read.value, type: 'blockTaskItem' })
|
||||
const blocks = [...marked, ...others]
|
||||
const task: AdfNode & { content: AdfNode[] } = { attrs: { state: marker.state }, content: [], type: 'blockTaskItem' }
|
||||
for (const [at, block] of blocks.entries()) {
|
||||
const read = readBlocks([block], reading, [...path, 'content', index], depth + 1)
|
||||
if (!read.ok) return { error: { ...read.error, path: splitPath(read.error.path, { block, next: blocks[at + 1], path, reading, task, tasks }) }, ok: false }
|
||||
for (const node of read.value) task.content.push(node)
|
||||
}
|
||||
tasks.push(task)
|
||||
}
|
||||
return success(splitTaskList({ content: tasks, type: 'taskList' }))
|
||||
}
|
||||
|
||||
type Refusal = { block: ReadBlock; next: ReadBlock | undefined; path: ConvertErrorPath; reading: Reading; task: AdfNode; tasks: readonly AdfNode[] }
|
||||
|
||||
// splitTaskList moves a task's children, so a refusal in `block` names where its node would stand: in place of the node, the split places a stand-in of its type.
|
||||
function splitPath(refused: ConvertErrorPath, { block, next, path, reading, task, tasks }: Refusal): ConvertErrorPath {
|
||||
const [, offset, ...within] = refused.slice(path.length + 2)
|
||||
const listIndex = path.at(-1)
|
||||
if (typeof offset !== 'number' || typeof listIndex !== 'number') return refused
|
||||
// A nested task list reads as a task list, then a block the split moves out, so a node past its first stands outside the task.
|
||||
const before = Array.from({ length: offset }, (_, at): AdfNode => ({ type: at === 0 ? 'taskList' : 'bulletList' }))
|
||||
const node: AdfNode & { content: [AdfNode] } = { content: [{ text: '', type: 'text' }], type: offset === 0 ? childType(block, reading) : 'bulletList' }
|
||||
const after = next === undefined ? [] : [{ type: childType(next, reading) }]
|
||||
const list = { content: [...tasks, { ...task, content: [...nodeContent(task), ...before, node, ...after] }], type: 'taskList' }
|
||||
const place = splitPlace(list, node)
|
||||
if (place === undefined) return refused
|
||||
const [blockIndex, ...steps] = place
|
||||
return [...path.slice(0, -1), listIndex + blockIndex, ...steps, ...within]
|
||||
}
|
||||
|
||||
// A task keeps only paragraphs and task lists, so another block's kind stands in for its node's type.
|
||||
function childType(block: ReadBlock, reading: Reading): string {
|
||||
if (block.kind === 'bulletList') return taskItems(block.items) === undefined ? 'bulletList' : 'taskList'
|
||||
if (block.kind !== 'paragraph') return block.kind === 'taskParagraph' ? 'paragraph' : block.kind
|
||||
const read = paragraphNode(block.text, reading, [])
|
||||
return read.ok ? read.value.type : 'paragraph'
|
||||
}
|
||||
|
||||
function directiveNode(block: DirectiveBlock, reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
||||
const read = readBlockDirectiveNode(block.name, block.argument, block.attributes, path)
|
||||
if (!read.ok) return read
|
||||
|
||||
@@ -180,6 +180,25 @@ test('stands a block a task item cannot hold, and what follows it in the item, a
|
||||
])
|
||||
})
|
||||
|
||||
test('names the place a refused block would stand once the task list moves it', () => {
|
||||
const refused = (markdown: string): string => {
|
||||
const parsed = portableMarkdownToAdf(markdown)
|
||||
return parsed.ok ? 'read' : parsed.error.path.map((step) => '/' + step).join('')
|
||||
}
|
||||
assert.equal(refused('- [x] A\n\n ```\n x\n ```\n\n <div>x</div>\n'), '/content/2')
|
||||
assert.equal(refused('- [ ] a <b>x</b>\n'), '/content/0/content/0')
|
||||
assert.equal(refused('- [ ] a <b>x</b>\n\n more\n'), '/content/0/content/0/content/0')
|
||||
assert.equal(refused('- [ ] a <b>x</b>\n\n \n'), '/content/0/content/0')
|
||||
assert.equal(refused('- [ ] A\n\n b <b>x</b>\n'), '/content/0/content/0/content/1')
|
||||
assert.equal(refused('- [ ] A\n\n # h\n\n b <b>x</b>\n'), '/content/2')
|
||||
assert.equal(refused('- [ ] A\n\n # h\n- [ ] B <b>x</b>\n'), '/content/2/content/0')
|
||||
assert.equal(refused('- [ ] A\n- [ ] B\n\n # h\n- [ ] C <b>x</b>\n'), '/content/2/content/0')
|
||||
assert.equal(refused('- [ ] A\n - [ ] C <b>x</b>\n'), '/content/0/content/1/content/0')
|
||||
assert.equal(refused('- [ ] A\n\n # h\n - [ ] C <b>x</b>\n'), '/content/2/content/0')
|
||||
assert.equal(refused('- [ ] A\n - [ ] C\n\n # h\n\n <div>x</div>\n'), '/content/2')
|
||||
assert.equal(refused('> q\n>\n> - [ ] A\n>\n> # h\n>\n> <div>x</div>\n'), '/content/0/content/3')
|
||||
})
|
||||
|
||||
test('reads a task list beside a list of another bullet character as two lists', () => {
|
||||
const tasks = (state: string, value: string): AdfNode => bare('taskList', node('taskItem', { state }, text(value)))
|
||||
assert.deepEqual(read('- [x] a\n* b\n'), [tasks('DONE', 'a'), bare('bulletList', bare('listItem', bare('paragraph', text('b'))))])
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AdfNode } from '../../adf/document.ts'
|
||||
import type { ConvertErrorPath } from '../../result.ts'
|
||||
import { isTaskItem } from '../../adf/block-nodes.ts'
|
||||
import { nodeContent } from '../../adf/document.ts'
|
||||
|
||||
@@ -18,6 +19,21 @@ export function splitTaskList(list: AdfNode): AdfNode[] {
|
||||
return blocks
|
||||
}
|
||||
|
||||
// Where `node`, a child of one of the list's tasks, stands among the blocks splitTaskList returns; a lone paragraph, holding its inline node, stands as its taskItem.
|
||||
export function splitPlace(list: AdfNode, node: AdfNode & { content: [AdfNode] }): [number, ...ConvertErrorPath] | undefined {
|
||||
const [inline] = node.content
|
||||
const places = splitTaskList(list).flatMap((block, index): [number, ...ConvertErrorPath][] => {
|
||||
if (block === node) return [[index]]
|
||||
if (block.type !== 'taskList') return []
|
||||
return nodeContent(block).flatMap((item, child): [number, ...ConvertErrorPath][] => {
|
||||
if (item === node || nodeContent(item)[0] === inline) return [[index, 'content', child]]
|
||||
const at = nodeContent(item).indexOf(node)
|
||||
return at === -1 ? [] : [[index, 'content', child, 'content', at]]
|
||||
})
|
||||
})
|
||||
return places[0]
|
||||
}
|
||||
|
||||
// An item is a task and the children up to the next one.
|
||||
function itemsOf(children: readonly AdfNode[]): AdfNode[][] {
|
||||
const items: AdfNode[][] = []
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
| ID | Release | Exempt | Item | R | S | A | G | Goals | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 93 | 0.2.0 | | **Report a refusal's `path` where the node would stand after `portableMarkdownToAdf` moves it out of a task item.** | 2 | 3 | 3 | 5 | 1 | 13.2 |
|
||||
| 86 | 0.2.0 | | **Write an image whose `media` only a link marks, and one captioned with plain text, in the CommonMark spelling a writer panel picks, in `adfToLosslessMarkdown`.** | 4 | 4 | 5 | 6 | 4, 6 | 13.0 |
|
||||
| 85 | 0.2.0 | defect | **Write an external image a link marks as `[](href)` in `adfToPortableMarkdown`, keeping the link target.** | 3 | 3 | 6 | 7 | 7 | 12.9 |
|
||||
| 89 | 0.2.1 | | **Build a `date`, `emoji`, `inlineCard`, `mention` or `status` holding no mark Atlassian's schema withholds from it, in every reader.** | 4 | 4 | 5 | 6 | 4 | 13.0 |
|
||||
@@ -70,13 +69,6 @@
|
||||
|
||||
## Details
|
||||
|
||||
### 93. Report a refusal's `path` where the node would stand after `portableMarkdownToAdf` moves it out of a task item.
|
||||
|
||||
`bulletBlocks` fixes a block's path before `splitTaskList` moves it beside or after its task, so
|
||||
`- [x] A\n\n ```\n x\n ```\n\n <div>x</div>\n` reports `/content/0/content/0/content/2`
|
||||
for a block that would stand at `/content/2`. The README says `path` is the node's place from the
|
||||
document root.
|
||||
|
||||
### 86. Write an image whose `media` only a link marks, and one captioned with plain text, in the CommonMark spelling a writer panel picks, in `adfToLosslessMarkdown`.
|
||||
|
||||
Every reader reads `[](href)` and `` as these shapes, and
|
||||
|
||||
Reference in New Issue
Block a user