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:
+2
-3
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user