mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 19:58:28 +01:00
Pin the portable flavour's callout and task reading and writing with tests
This commit is contained in:
@@ -491,3 +491,31 @@ test('writes a text node holding content the same in either order beside its nei
|
||||
assert.equal(portable(paragraph(holding, text('b'))), 'ab\n')
|
||||
assert.equal(portable(paragraph(text('b'), holding)), 'ba\n')
|
||||
})
|
||||
|
||||
test('writes a task list nested to the deepest level the reader carries, and refuses one more at the item holding it', () => {
|
||||
const task = (): AdfNode => node('taskItem', { state: 'TODO' }, text('a'))
|
||||
const nested = (levels: number): AdfNode => {
|
||||
let list: AdfNode = { content: [task()], type: 'taskList' }
|
||||
for (let level = 1; level < levels; level += 1) list = { content: [task(), list], type: 'taskList' }
|
||||
return list
|
||||
}
|
||||
const written = portable(nested(largestNesting))
|
||||
assert.ok(portableMarkdownToAdf(written).ok)
|
||||
assert.equal(written.split('\n').length, largestNesting + 1)
|
||||
const refused = adfToPortableMarkdown(document(nested(largestNesting + 1)))
|
||||
assert.deepEqual(refused.ok ? undefined : [refused.error.code, refused.error.path], ['unsupported-nesting-depth', ['content', 0, ...Array.from({ length: largestNesting }, () => ['content', 1]).flat(), 'content', 0]])
|
||||
const quoted = (levels: number): string => portable(node('panel', { panelType: 'info' }, nested(levels)))
|
||||
assert.ok(portableMarkdownToAdf(quoted(largestNesting - 1)).ok)
|
||||
assert.match(quoted(largestNesting), /^unsupported-nesting-depth at \//)
|
||||
})
|
||||
|
||||
test('writes a task list nested in the task before it, and more than one beside each other in that task', () => {
|
||||
const done = (value: string): AdfNode => node('taskItem', { state: 'DONE' }, text(value))
|
||||
const list = (...content: AdfNode[]): AdfNode => ({ content, type: 'taskList' })
|
||||
assert.equal(portable(list(done('Parent'), list(done('Child')), done('Next'))), '- [x] Parent\n - [x] Child\n- [x] Next\n')
|
||||
assert.equal(portable(list(done('Parent'), list(done('A')), list(done('B')))), '- [x] Parent\n - [x] A\n * [x] B\n')
|
||||
})
|
||||
|
||||
test('writes a task list opening with a list as that list in an item of its own', () => {
|
||||
assert.equal(portable({ content: [{ content: [node('taskItem', { state: 'TODO' }, text('a'))], type: 'taskList' }], type: 'taskList' }), '- - [ ] a\n')
|
||||
})
|
||||
|
||||
@@ -218,6 +218,71 @@ test('refuses a task list nested one level past the 500 the parser carries, at i
|
||||
assert.equal(quoted.ok ? undefined : quoted.error.code, 'unsupported-nesting-depth')
|
||||
})
|
||||
|
||||
test('keeps a backslash a title line ends in as text, a break only before a following line', () => {
|
||||
assert.deepEqual(read('> [!NOTE]- Trail\\\n'), [node('expand', { title: 'Trail\\' }, paragraph())])
|
||||
assert.deepEqual(read('> [!NOTE]- a \\* b\n> body\n'), [node('expand', { title: 'a * b' }, said('body'))])
|
||||
assert.deepEqual(read('> [!NOTE]- x\\\\\n> body\n'), [node('expand', { title: 'x\\' }, said('body'))])
|
||||
})
|
||||
|
||||
test('keeps the target of a link in a title whichever marks sit beside the link', () => {
|
||||
const titled = (markdown: string): unknown => {
|
||||
const blocks = read(markdown)
|
||||
return typeof blocks === 'string' ? blocks : blocks[0]?.attrs?.['title']
|
||||
}
|
||||
assert.equal(titled('> [!faq]- ~~[x](http://y)~~ b\n'), 'x (http://y) b')
|
||||
assert.equal(titled('> [!faq]- ==[x](http://y)== b\n'), 'x (http://y) b')
|
||||
assert.equal(titled('> [!faq]- *[x](http://y)* and [**z**](http://w)\n'), 'x (http://y) and z (http://w)')
|
||||
assert.equal(titled('> [!faq]- **[x](http://y)** and [*z*](http://w)\n'), 'x (http://y) and z (http://w)')
|
||||
assert.equal(titled('> [!faq]- [x](http://y)~~z~~\n'), 'x (http://y)z')
|
||||
assert.equal(titled('> [!faq]- \n'), 'a (http://u)')
|
||||
})
|
||||
|
||||
test('refuses what a callout title or body holds that no node spells, at the path of the paragraph or the callout', () => {
|
||||
const refused = (markdown: string): string => {
|
||||
const parsed = portableMarkdownToAdf(markdown)
|
||||
return parsed.ok ? 'read' : `${parsed.error.code} ${parsed.error.path.map((step) => '/' + step).join('')}`
|
||||
}
|
||||
assert.equal(refused('> [!faq]- a <b>x</b>\n'), 'unmappable-html /content/0')
|
||||
assert.equal(refused('> [!NOTE]\n>\n> a <b>x</b>\n'), 'unmappable-html /content/0/content/0')
|
||||
assert.equal(refused('> [!NOTE] first\n>\n> a <b>x</b>\n'), 'unmappable-html /content/0/content/1')
|
||||
assert.equal(refused('> [!NOTE]-\n>\n> a <b>x</b>\n'), 'unmappable-html /content/0/content/0')
|
||||
assert.equal(refused('> [!NOTE]- t\n>\n> > [!NOTE]-\n> >\n> > a <b>x</b>\n'), 'unmappable-html /content/0/content/0/content/0')
|
||||
assert.equal(refused('p\n\n> [!NOTE]\n>\n> a <b>x</b>\n'), 'unmappable-html /content/1/content/0')
|
||||
})
|
||||
|
||||
test('refuses a callout nested one level past the 500 the parser carries, a panel and an expand alike', () => {
|
||||
const nested = (marker: string, levels: number): string => Array.from({ length: levels }, (_, level) => `${'> '.repeat(level + 1)}${marker}\n${'> '.repeat(level + 1)}\n`).join('')
|
||||
for (const marker of ['[!NOTE]', '[!NOTE]-', '[!NOTE]- t']) {
|
||||
assert.ok(portableMarkdownToAdf(nested(marker, largestNesting)).ok, marker)
|
||||
const refused = portableMarkdownToAdf(nested(marker, largestNesting + 1))
|
||||
assert.equal(refused.ok ? undefined : refused.error.code, 'unsupported-nesting-depth', marker)
|
||||
}
|
||||
})
|
||||
|
||||
test('places a refusal in a bullet list by the list and the item holding it', () => {
|
||||
const refused = (markdown: string): string => {
|
||||
const parsed = portableMarkdownToAdf(markdown)
|
||||
return parsed.ok ? 'read' : parsed.error.path.map((step) => '/' + step).join('')
|
||||
}
|
||||
assert.equal(refused('p\n\n- a\n\n- b <b>x</b>\n'), '/content/1/content/1/content/0')
|
||||
assert.equal(refused('- [x] a\n- b <b>x</b>\n'), '/content/0/content/1/content/0')
|
||||
assert.equal(refused('q\n\n- [x] a\n- [ ] b <b>x</b>\n'), '/content/1/content/1')
|
||||
})
|
||||
|
||||
test('reads a task whose marker line holds only breaks and spaces by the lines after it', () => {
|
||||
assert.deepEqual(read('- [ ] a\n b\n'), [bare('taskList', task('TODO', text('a b')))])
|
||||
assert.deepEqual(read('- [ ] \\\n \\\n x\n'), [bare('taskList', task('TODO', text('x')))])
|
||||
assert.deepEqual(read('- [x]\n\n Second.\n- [ ] Next\n'), [bare('taskList', task('DONE', text('Second.')), task('TODO', text('Next')))])
|
||||
assert.deepEqual(read('- [x]\n\n First.\n\n Second.\n'), [bare('taskList', node('blockTaskItem', { state: 'DONE' }, said('First.'), said('Second.')))])
|
||||
assert.deepEqual(read('- [x]\n- [ ]\n'), [bare('taskList', task('DONE'), task('TODO'))])
|
||||
})
|
||||
|
||||
test('reads a lone image after a task marker as its alt text linked', () => {
|
||||
const link: AdfMark = { attrs: { href: 'http://u' }, type: 'link' }
|
||||
assert.deepEqual(read('- [ ] \n'), [bare('taskList', task('TODO', text('alt', link)))])
|
||||
assert.deepEqual(read('- [ ]  b\n'), [bare('taskList', task('TODO', text('alt', link), text(' b')))])
|
||||
})
|
||||
|
||||
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'))))])
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
|
||||
`Bar = 0.1`
|
||||
|
||||
`Next ID = 175`
|
||||
`Next ID = 176`
|
||||
|
||||
## Items
|
||||
|
||||
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 153 | 0.4.0 | | question | **Decide whether `src/markdown/` root may import `emit/`, `parse/` or `portable/`, and hold the answer in `.oxlintrc.json` and §The source parts by ADF and format.** | 1 | 2 | 2 | 3 | 2 | code health by 3 | 26.2 |
|
||||
| 128 | 0.4.0 | | | **Pin the portable flavour's block reading and writing with tests: callout titles past one line, a link beside other marks in a title, the nesting limit of callouts and task lists, refusals inside tasks and their paths, and empty or non-paragraph tasks.** | 2 | 1 | 1 | 2 | 3 | code health by 3 | 19.6 |
|
||||
| 75 | 0.4.0 | | | **Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.** | 1 | 1 | 1 | 1 | 2 | code health by 2 | 17.4 |
|
||||
| 154 | 0.4.0 | | | **Pair highlights over the inline reader's pieces by their offsets before building nodes, so no placeholder text node stands in for a delimiter.** | 2 | 1 | 2 | 3 | 4 | code health by 4 | 17.4 |
|
||||
| 155 | 0.4.0 | | | **Have `closeBracket` return the link, image or literal bracket it builds, and apply it to the scan in one caller.** | 2 | 1 | 2 | 3 | 4 | code health by 4 | 17.4 |
|
||||
| 162 | 0.4.0 | | | **Pin each rule of the emphasis sweep with a hand-worked line, so every mutant that lets a pair fall where the one-run-per-pass reference keeps it fails a test.** | 2 | 1 | 1 | 2 | 4 | code health by 4 | 17.4 |
|
||||
| 175 | 0.4.0 | | | **Delete the guards in `tryTaskList` that `reduceToPortable` makes unreachable, once a test shows `adfToPortableMarkdown` reaches none of them.** | 1 | 1 | 1 | 1 | 2 | code health by 2 | 17.4 |
|
||||
| 45 | 0.4.0 | | defect | **Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** | 2 | 1 | 3 | 4 | 3 | 1 by 2 | 13.1 |
|
||||
| 127 | 0.4.0 | | principle | **Hold what `entity-references.test.ts`, `json-text.test.ts` and `result.test.ts` check by asserting outcomes, so none reads source text.** | 2 | 2 | 1 | 3 | 3 | code health by 2 | 13.1 |
|
||||
| 159 | 0.4.0 | | | **Delete `taskBlocks`'s branch that writes the task marker as its own paragraph, once a test shows no block task item reaches it.** | 1 | 1 | 1 | 1 | 1 | code health by 1 | 10.5 |
|
||||
@@ -75,14 +75,6 @@ a paragraph opening with a spellable `!adf:link` (§Nothing recurses unbounded).
|
||||
`src/markdown/**` bans only `html/`; banning `emit/`, `parse/` and `portable/` for the root's own
|
||||
files, with its `ci.sh` probe, extends the decision.
|
||||
|
||||
### 128. Pin the portable flavour's block reading and writing with tests: callout titles past one line, a link beside other marks in a title, the nesting limit of callouts and task lists, refusals inside tasks and their paths, and empty or non-paragraph tasks.
|
||||
|
||||
Mutation testing left about 40 mutants alive in code moved verbatim into `src/markdown/portable/`
|
||||
on 2026-10-22, so the gaps predate the move: `tryTaskList` and `taskBlocks` in `src/markdown/portable/block-spelling.ts`, and
|
||||
`markerLine`, `quoteReader`, `titleText`, `taskBlocks` and `readTaskBlock` in
|
||||
`src/markdown/portable/portable-markdown-to-adf.ts`. Each test takes its expected value from
|
||||
`spec/flavour.md` or `docs/decisions.md`, never from the code.
|
||||
|
||||
### 75. Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.
|
||||
|
||||
`blockCarry` and `hostileBlockCarry` (`src/conformance/markdown-property.test.ts`) write a fence
|
||||
@@ -119,6 +111,15 @@ random seed. Write one line per rule of that entry, its expected markdown worked
|
||||
never from the code, then run Stryker on the file. A mutant that only takes fewer runs back costs a
|
||||
pass and may survive.
|
||||
|
||||
### 175. Delete the guards in `tryTaskList` that `reduceToPortable` makes unreachable, once a test shows `adfToPortableMarkdown` reaches none of them.
|
||||
|
||||
`reduceToPortable` runs first and turns a task list opening with a non-task into a bullet list. So
|
||||
`tryTaskList` (`portable/block-spelling.ts`) never sees `previous === undefined`, and its
|
||||
`'a task holds blocks no list item spells'` failure has no test. The `path` it passes to
|
||||
`taskBlocks` and `placedBlock`, and its `Math.min` of `headroom`, change no output, since the
|
||||
nesting refusal's path comes from `reduceToPortable`. Found by mutation testing for item 128,
|
||||
2026-11-08.
|
||||
|
||||
### 45. Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.
|
||||
|
||||
Lands with item 77: the reader returns item 77's copy. Goal 1 has every call return a result; the
|
||||
|
||||
Reference in New Issue
Block a user