3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +01:00

Answer prose round 1: a task item keeps the task lists right after its paragraphs nested, and the README, changelog, decision, comment, item 49 and a test title say it plainly

This commit is contained in:
2026-10-09 13:27:40 +02:00
committed by Lillem4n
parent 0c56a479bb
commit cd6cb8bece
9 changed files with 32 additions and 28 deletions
+2 -3
View File
@@ -52,9 +52,8 @@
brackets literal text, where `0.1.0` split the outer link around it; see `MIGRATION.md`.
- Add `adfToPortableMarkdown` and `portableMarkdownToAdf`, a lossy pair converting ADF to and from
markdown GitHub, GitLab and Obsidian render: alerts, callouts, task lists, `==highlights==` and
pipe tables. A task item holds only paragraphs: from its first other block, such as an image or
a code block, the rest of the item stands after the task list; a nested task list before that
block stays nested.
pipe tables. A task item keeps its paragraphs, then its nested task lists; the rest of the item,
such as an image or a code block, stands after the task list.
- Spell `rule`'s `color`, `style` and `weight`, `layoutSection`'s `columnRuleStyle` and a link's
`collection`, `id` and `occurrenceKey` directly where they rode the opaque carry.
- Fix an image inside another image's description: it flattens into the alt text, where it was
+1 -1
View File
@@ -127,7 +127,7 @@ saving what this pair read replaces mentions, attachments and macros with text.
| --- | --- | --- |
| `panel` | a GitHub alert, `> [!WARNING]`: info `NOTE`, note `IMPORTANT`, tip and success `TIP`, warning `WARNING`, error `CAUTION`, custom `NOTE` | `NOTE` info, `IMPORTANT` note, `TIP` tip, `WARNING` warning, `CAUTION` error, and Obsidian's: hint tip; success, check, done success; attention warning; danger, failure, fail, missing, bug, error error; any other word info — in any case; the rest of the marker's line is the first paragraph |
| `expand`, `nestedExpand` | Obsidian's folded callout, `> [!NOTE]- Title` | `-` or `+` after any word, the rest of the marker's line the title; a link reads `text (target)`, or its text alone where the text is the target with or without `mailto:`; an expand inside an expand is a `nestedExpand` |
| `taskList` | `- [x] Done`, `- [ ] Todo`; from an item's first block other than a paragraph, the rest of the item after the list, which resumes at the next task | a bullet list whose every item is so marked, `[X]` too; from an item's first block other than a paragraph or a nested task list, the rest of the item after the list, which resumes at the next task |
| `taskList` | `- [x] Done`, `- [ ] Todo`. An item keeps its paragraphs, then its nested task lists; the rest of the item stands after the list, which resumes at the next task | a bullet list whose every item is so marked, `[X]` too. An item keeps its paragraphs, then its nested task lists; the rest of the item stands after the list, which resumes at the next task |
| `backgroundColor` | `==text==` | `==text==` on one line, the text touching both delimiters, bounded outside by whitespace, punctuation or a line edge, or touching a Han, Hangul, Hiragana, Katakana, Thai, Lao, Khmer or Myanmar character on either side, in the editor's default highlight `#f8e6a0` |
| `table` | a pipe table: the first row its header, a cell's blocks on one line, a span kept under its header by empty cells | — |
| `decisionList` | a bullet list | — |
+6 -7
View File
@@ -252,13 +252,12 @@ markdown pasted twice into one document repeats its ids: determinism wins over t
2026-10-09, a reader panel, 6 of 7. Goals 4, 6 and 7. Valid while Atlassian's schema holds a
`blockTaskItem` to paragraphs and extensions.
In `portableMarkdownToAdf` and `adfToPortableMarkdown`, a task item keeps its paragraphs up to its
first other block. That block, the item's blocks after it and the task lists nested in the item
after that block stand after the task list, which resumes at the next task. The panel of two bots,
two LLM pipelines, a support agent, a product manager and an engineer, shown an image, a code block
and a nested bullet list in a task item, picked this over turning those blocks into text inside
the item (1 of 7) and over a plain bullet list keeping the markers as text (none): every reader
wanted the checkboxes, and six wanted the blocks in their own form.
In `portableMarkdownToAdf` and `adfToPortableMarkdown`, a task item keeps its paragraphs, then its
nested task lists. The rest of the item stands after the task list. The list resumes at the next
task. The panel of two bots, two LLM pipelines, a support agent, a product manager and an engineer,
shown an image, a code block and a nested bullet list in a task item, picked this over turning those
blocks into text inside the item (1 of 7) and over a plain bullet list keeping the markers as text
(none): every reader wanted the checkboxes, and six wanted the blocks in their own form.
## A callout title keeps its link targets
+1 -1
View File
@@ -48,7 +48,7 @@ test('a generated document emits markdown that reads back to it', { timeout: pro
)
})
test('a generated document writes portable markdown refusing only what the guard refuses, and that markdown reads back to its text and to itself, a block task item holding paragraphs alone', { timeout: propertyTimeout }, () => {
test('a generated document writes portable markdown refusing only what the guard refuses, and that markdown reads back to its text, to itself and to block task items of paragraphs alone', { timeout: propertyTimeout }, () => {
fc.assert(
fc.property(adfDocument, (document) => {
const written = adfToPortableMarkdown(document)
+1 -5
View File
@@ -198,7 +198,6 @@ function filledNode(node: AdfNode, content: Result<AdfNode[]>): Result<AdfNode>
return success({ ...node, content: content.value.length === 0 ? [{ type: 'paragraph' }] : content.value })
}
// A task list trailing an item's blocks stands beside it, as ADF nests one.
function bulletNode(items: readonly Block[][], reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode> {
const led = []
for (const [first, ...others] of items) {
@@ -212,10 +211,7 @@ function bulletNode(items: readonly Block[][], reading: Reading, path: ConvertEr
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
let beside = read.value.length
while (read.value[beside - 1]?.type === 'taskList') beside -= 1
tasks.push({ attrs: { state: marker.state }, content: read.value.slice(0, beside), type: 'blockTaskItem' })
for (const nested of read.value.slice(beside)) tasks.push(nested)
tasks.push({ attrs: { state: marker.state }, content: read.value, type: 'blockTaskItem' })
}
return success({ content: tasks, type: 'taskList' })
}
@@ -167,6 +167,11 @@ test('stands a block a task item cannot hold, and what follows it in the item, a
bare('codeBlock', text('x')),
])
assert.deepEqual(read('- [x] A\n - plain\n- [ ] B\n'), [bare('taskList', task('DONE', text('A'))), bare('bulletList', bare('listItem', said('plain'))), bare('taskList', task('TODO', text('B')))])
assert.deepEqual(read('- [ ] A\n - [ ] Child\n\n # h\n- [ ] B\n'), [
bare('taskList', task('TODO', text('A')), bare('taskList', task('TODO', text('Child')))),
node('heading', { level: 1 }, text('h')),
bare('taskList', task('TODO', text('B'))),
])
assert.deepEqual(read('- [ ] A\n\n # h\n - [ ] Child\n- [ ] B\n'), [
bare('taskList', task('TODO', text('A'))),
node('heading', { level: 1 }, text('h')),
@@ -304,17 +304,17 @@ function reduceTaskList(node: AdfNode, reduction: Reduction): Result<AdfNode[]>
continue
}
if (isTask(child)) {
nestIn(tasks, nested)
pushMerged(tasks, nested)
nested = []
}
for (const block of blocks) (isTask(child) ? tasks : nested).push(block)
}
nestIn(tasks, nested)
pushMerged(tasks, nested)
if (regular) return success(splitTaskList({ content: tasks, type: 'taskList' }))
return success(listOf(nodeContent(tasksAsText({ content: tasks, type: 'taskList' })), 'bulletList'))
}
function nestIn(tasks: AdfNode[], nested: readonly AdfNode[]): void {
function pushMerged(tasks: AdfNode[], nested: readonly AdfNode[]): void {
for (const block of mergedLists(nested)) tasks.push(block)
}
+8 -4
View File
@@ -1,7 +1,7 @@
import type { AdfNode } from '../../adf/document.ts'
import { nodeContent } from '../../adf/document.ts'
// Atlassian's schema holds a block task item to paragraphs: from its first other block, the item's blocks and the lists beside it stand after the list, which resumes at the next task.
// Atlassian's schema holds a blockTaskItem to paragraphs and extensions, and nests a task list beside its item.
export function splitTaskList(list: AdfNode): AdfNode[] {
const blocks: AdfNode[] = []
let tasks: AdfNode[] = []
@@ -12,9 +12,13 @@ export function splitTaskList(list: AdfNode): AdfNode[] {
let stands: readonly AdfNode[] = standing || !(isTask || child.type === 'taskList') ? [child] : []
if (child.type === 'blockTaskItem') {
const content = nodeContent(child)
const held = content.findIndex((block) => block.type !== 'paragraph')
tasks.push(heldTask(child, held === -1 ? content : content.slice(0, held)))
stands = held === -1 ? [] : content.slice(held)
let held = 0
while (content[held]?.type === 'paragraph') held += 1
let nested = held
while (content[nested]?.type === 'taskList') nested += 1
tasks.push(heldTask(child, content.slice(0, held)))
for (const taskList of content.slice(held, nested)) tasks.push(taskList)
stands = content.slice(nested)
} else if (stands.length === 0) tasks.push(child)
if (stands.length === 0) continue
if (tasks.length > 0) blocks.push({ ...list, content: tasks })
+5 -4
View File
@@ -75,10 +75,11 @@ Today `- a` then `+ b`, or `1.` then `1)`, reads as one list; CommonMark reads t
301 and 302). Goal 5 settles it for the lossless flavour too: CommonMark spells adjacent lists by
changing the marker, so `adfToLosslessMarkdown` and `adfToPortableMarkdown` alternate `-` and `*`
between adjacent bullet lists, a task list among them, and `.` and `)` between adjacent ordered
lists, and `!adf:listBreak` retires. Until then, a bullet list standing after a task list joins
it when written, so `adfToPortableMarkdown` writes every task's marker as text. `*` is the maintainer's pick (2026-10-04). Breaking: `MIGRATION.md`'s Readings and
Spellings tables gain their rows. Examples 301 and 302 lose their `pending` exceptions, and the
spelling leaves the README's "Seven CommonMark spellings" bullet, which counts one fewer.
lists, and `!adf:listBreak` retires. `*` is the maintainer's pick (2026-10-04). Breaking:
`MIGRATION.md`'s Readings and Spellings tables gain their rows. Examples 301 and 302 lose their
`pending` exceptions, and the spelling leaves the README's "Seven CommonMark spellings" bullet,
which counts one fewer. Until this lands, a bullet list written right after a task list would read
back as part of it, so `adfToPortableMarkdown` writes that task list's markers as text.
### 61. Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.