Skip to content

Checkbox task lists (- [x]/- [ ]) lose check state — converted to plain bulletList instead of taskList - #10

Open
acreeger wants to merge 2 commits into
JeromeErasmus:mainfrom
acreeger:fix/issue-8__checkbox-task-list
Open

Checkbox task lists (- [x]/- [ ]) lose check state — converted to plain bulletList instead of taskList#10
acreeger wants to merge 2 commits into
JeromeErasmus:mainfrom
acreeger:fix/issue-8__checkbox-task-list

Conversation

@acreeger

Copy link
Copy Markdown

Fixes #8

Checkbox task lists (- [x]/- [ ]) lose check state — converted to plain bulletList instead of taskList

Markdown checkbox syntax is converted to regular bulletList/listItem nodes, losing the checked/unchecked state entirely. ADF supports taskList/taskItem nodes with a state attribute (TODO/DONE) for this.

Input:

- [x] Completed task
- [ ] Pending task

Current output:

{
  "type": "bulletList",
  "content": [
    { "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Completed task" }] }] },
    { "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Pending task" }] }] }
  ]
}

The [x] and [ ] markers are stripped entirely — no trace remains in the ADF output. The checkbox state is lost.

Expected output:

{
  "type": "taskList",
  "attrs": { "localId": "unique-id" },
  "content": [
    { "type": "taskItem", "attrs": { "localId": "unique-id-1", "state": "DONE" }, "content": [{ "type": "text", "text": "Completed task" }] },
    { "type": "taskItem", "attrs": { "localId": "unique-id-2", "state": "TODO" }, "content": [{ "type": "text", "text": "Pending task" }] }
  ]
}

This PR was created automatically by iloom.

@acreeger

acreeger commented Feb 16, 2026

Copy link
Copy Markdown
Author

Complexity Assessment - Issue #8

Analyzing: Checkbox task lists (- [x]/- [ ]) lose check state

Analysis Plan

  • Understand current parser architecture
  • Identify files handling list parsing
  • Assess scope of changes needed
  • Determine file architecture quality
  • Finalize complexity classification

Key Findings

Files Affected (Estimated 7-8):

  1. /src/parser/markdown-to-adf/ASTBuilder.ts (2578 LOC - LARGE FILE)
  2. /src/types/adf.types.ts - Add TaskListNode, TaskItemNode types
  3. /src/parser/markdown-to-adf/types.ts - Token definitions
  4. /src/parser/remark/adf-from-markdown.ts - May need updates
  5. /src/parser/adf-to-markdown/nodes/TaskListConverter.ts (NEW)
  6. /src/parser/adf-to-markdown/nodes/TaskItemConverter.ts (NEW)
  7. /src/parser/ConverterRegistry.ts - Register new converters
  8. Test file - New test coverage

Complexity Indicators:

  • ASTBuilder is a 2578 LOC file (high cognitive load risk)
  • Bidirectional conversion required (MD→ADF and ADF→MD)
  • Multi-layer parsing pipeline: remark → mdast → tokens → ASTBuilder
  • Requires new node types with full converter infrastructure
  • Checkbox detection logic must differentiate from regular bullet lists
  • Unique localId generation needed for ADF compliance
  • Conversion registry updates needed

Architecture Quality:

  • ASTBuilder contains mixed concerns (multiple node conversion methods)
  • Parsing logic distributed across multiple layers
  • No existing checkbox/task list patterns to follow

Reasoning: Multiple files affected (>5), including a large 2578-line file with mixed responsibilities, combined with bidirectional conversion needs and multi-layer pipeline changes make this COMPLEX. High cognitive load and potential for breaking existing list parsing.


Complexity Assessment

Classification: COMPLEX

Metrics:

  • Estimated files affected: 7-8
  • Estimated lines of code: 250-350 (new code)
  • Breaking changes: No
  • Database migrations: No
  • Cross-cutting changes: Yes (parameter/state flow through MD→token→ADF→converter layers)
  • File architecture quality: Poor (ASTBuilder 2578 LOC with mixed concerns)
  • Architectural signals triggered: Multi-layer pipeline coordination, bidirectional conversion, unique ID generation
  • Overall risk level: Medium

Reasoning: Implementation requires coordinating changes across multiple architectural layers (markdown parsing → token conversion → ADF building → reverse conversion) and modifying a large mixed-concern file (ASTBuilder). Bidirectional conversion adds complexity. Cross-cutting state flow (checkbox state) propagates through 4+ layers.

@acreeger

acreeger commented Feb 16, 2026

Copy link
Copy Markdown
Author

Analysis Phase

  • Fetch issue details and review existing context
  • Problem space research (ADF taskList/taskItem spec, markdown-it plugin behavior)
  • Codebase research: ASTBuilder list/listItem conversion logic
  • Codebase research: Trace markdown parsing pipeline (remark-gfm task list support)
  • Codebase research: Reverse converter infrastructure (ADF -> Markdown)
  • Codebase research: Type definitions, validator, and registry patterns
  • Cross-cutting change analysis (data flow mapping)
  • Document findings

Executive Summary

Markdown checkbox syntax (- [x]/- [ ]) is fully parsed by remark-gfm (already a dependency) which sets checked: true/false on mdast listItem nodes. However, the ASTBuilder ignores this property and always emits bulletList/listItem ADF nodes. The fix requires a cross-cutting change across 8+ files spanning both conversion directions: new ADF type definitions, conditional list/listItem-to-taskList/taskItem conversion in ASTBuilder, new reverse converters (ADF taskList/taskItem to markdown), registry registration, schema updates, and the token-based parser path.

Questions and Key Decisions

Question Answer
Should localId attributes on taskList/taskItem be deterministic (e.g., hash-based) or random UUIDs? Random UUIDs (crypto.randomUUID() or equivalent) are the standard Atlassian approach. No existing ID generation utility exists in the codebase, so one must be created. Node's crypto.randomUUID() is available in Node >= 19 (project requires >= 20.11.1).
Should mixed lists (some items checked, some not) produce a taskList or remain bulletList? Per Atlassian ADF spec, a taskList can contain items in both TODO and DONE states. If any item in a list has checked !== null, the entire list should become a taskList.
Should the MarkdownTokenizer (token-based) path also support checkbox detection? Yes -- both paths (mdast-based via MarkdownToAdfEngine, and token-based via MarkdownTokenizer) are active. The token-based path is used inside ADF fence blocks (parseBlockContent in ASTBuilder).

HIGH/CRITICAL Risks

  • ADF Schema validation will reject taskList/taskItem: The JSON schema at src/validators/schemas/adf-schema.json:36-39 has a strict type enum that does not include taskList or taskItem. Any generated ADF containing these types will fail validation until the schema is updated.

Impact Summary

  • 2 new converter files to create (TaskListConverter, TaskItemConverter)
  • ~8 existing files requiring modification
  • 1 new utility function (localId generation)
  • JSON schema update required
  • Both parser paths (mdast-based and token-based) affected

Complete Technical Reference (click to expand for implementation details)

Problem Space Research

Problem Understanding

Markdown task lists (- [x]/- [ ]) are a GFM extension widely used in issue trackers and documentation. Atlassian Document Format (ADF) natively supports task lists via taskList/taskItem nodes with a state attribute (TODO/DONE). Currently, the parser strips checkbox markers entirely, producing plain bulletList/listItem nodes.

Architectural Context

The parser has two conversion directions:

  1. Markdown to ADF (md-to-adf): Uses remark/mdast pipeline (MarkdownToAdfEngine) and a fallback token-based parser (MarkdownTokenizer). Both feed into ASTBuilder.
  2. ADF to Markdown (adf-to-md): Uses ConverterRegistry with per-node-type converter classes (AdfToMarkdownEngine).

Both directions must be updated for round-trip fidelity.

Edge Cases Identified

  • Mixed lists: Some items with checkboxes, some without -- the entire list should become taskList if checked property is present on the mdast list node (remark-gfm does this).
  • Nested task lists inside other lists or block elements (panels, expands).
  • Task items with rich inline content (bold, links, emoji).
  • The token-based parser path used inside parseBlockContent() for ADF fence block content.

Third-Party Research Findings

remark-gfm v4.0.1 (already installed)

Source: Codebase dependency analysis + mdast type definitions
Key Findings:

  • Includes micromark-extension-gfm-task-list-item and mdast-util-gfm-task-list-item
  • Sets checked: boolean | null | undefined on mdast ListItem nodes (see @types/mdast/index.d.ts:899)
  • checked: true = [x], checked: false = [ ], checked: null/undefined = regular list item
  • No additional plugins or configuration needed -- task list parsing is already active

Atlassian ADF taskList/taskItem Spec

Source: Issue description links
Key Findings:

  • taskList node: requires attrs.localId (string), contains taskItem children
  • taskItem node: requires attrs.localId (string) and attrs.state ("TODO" | "DONE"), contains inline content directly (no wrapping paragraph)
  • Critical: taskItem content is inline nodes directly, NOT wrapped in a paragraph (unlike listItem)

Codebase Research Findings

Affected Area: Markdown-to-ADF (mdast path)

Entry Point: src/parser/engines/MarkdownToAdfEngine.ts:100-109 - convert() parses markdown, runs through remark pipeline, calls astBuilder.buildADFFromMdast()

ASTBuilder.convertMdastList at src/parser/markdown-to-adf/ASTBuilder.ts:1630-1636:

  • Always emits bulletList or orderedList based on node.ordered
  • Does not check node.children[].checked property
  • This is the primary point where task list detection must occur

ASTBuilder.convertMdastListItem at src/parser/markdown-to-adf/ASTBuilder.ts:1638-1643:

  • Always emits listItem with children converted via convertMdastNodesToADF
  • Does not check node.checked property
  • Must be modified to emit taskItem with state and localId attrs, and to NOT wrap content in paragraph

Affected Area: Markdown-to-ADF (token path)

MarkdownTokenizer.parseList at src/parser/markdown-to-adf/MarkdownTokenizer.ts:461-490:

  • Detects lists via regex /^[-*+]\s/ and /^\d+\.\s/
  • Does not detect checkbox syntax [x]/[ ]

MarkdownTokenizer.parseListItem at src/parser/markdown-to-adf/MarkdownTokenizer.ts:492-538:

  • Extracts content via regex /^\s*(?:[-*+]|\d+\.)\s+(.*)$/ at line 501
  • The [x] text or [ ] text ends up in the captured group -- checkbox markers become part of the text content
  • No detection or stripping of checkbox markers

ASTBuilder.convertList at src/parser/markdown-to-adf/ASTBuilder.ts:234-255:

  • Token-based list conversion, always emits bulletList or orderedList
  • Does not check for checkbox tokens

ASTBuilder.convertListItem at src/parser/markdown-to-adf/ASTBuilder.ts:257-274:

  • Token-based list item conversion, always emits listItem

ASTBuilder.parseBlockContent at src/parser/markdown-to-adf/ASTBuilder.ts:2300-2440:

  • Fallback block parser used inside ADF fence blocks
  • Has bullet list detection at line 2405-2427 but no checkbox awareness

Affected Area: ADF-to-Markdown (reverse direction)

AdfToMarkdownEngine.registerConverters at src/parser/engines/AdfToMarkdownEngine.ts:212-255:

  • Registers all node/mark converters -- no taskList or taskItem converters exist
  • Must register new TaskListConverter and TaskItemConverter

Parser.registerConverters at src/parser/Parser.ts:267-310:

  • Duplicate registration (legacy support) -- must also register new converters here

BulletListConverter at src/parser/adf-to-markdown/nodes/BulletListConverter.ts:

  • Pattern reference for implementing TaskListConverter
  • Iterates content, delegates to listItem converter via registry

ListItemConverter at src/parser/adf-to-markdown/nodes/ListItemConverter.ts:

  • Pattern reference for implementing TaskItemConverter
  • Prefixes with - , handles multi-line indentation
  • TaskItemConverter should prefix with - [x] or - [ ] based on state attr

Affected Area: Type Definitions

adf.types.ts at src/types/adf.types.ts:61-77:

  • Defines BulletListNode, OrderedListNode, ListItemNode
  • Missing: TaskListNode, TaskItemNode type definitions

Affected Area: JSON Schema

adf-schema.json at src/validators/schemas/adf-schema.json:36-39:

  • Node type enum: ["paragraph", "heading", "blockquote", "bulletList", "orderedList", "listItem", "codeBlock", "panel", "expand", "table", "tableRow", "tableHeader", "tableCell", "mediaSingle", "mediaGroup", "media", "rule", "text", "hardBreak", "mention", "emoji", "status", "date", "inlineCard"]
  • Missing: taskList, taskItem
  • Must add these to the enum AND add validation rules for their required attrs

Affected Area: ASTBuilder mdast-to-ADF roundtrip

ASTBuilder.convertAdfNodeToMdast at src/parser/markdown-to-adf/ASTBuilder.ts:2003-2122:

  • Handles conversion back to mdast for remark stringify path
  • Only handles known types (heading, paragraph, panel, media, mediaSingle)
  • Falls through to default for unknown types
  • May need explicit taskList/taskItem handling for the remark stringify path

Architectural Flow Analysis

Data Flow: Checkbox State (Markdown to ADF, mdast path)

Entry Point: src/parser/engines/MarkdownToAdfEngine.ts:100 - convert(markdown)
Flow Path:

  1. MarkdownToAdfEngine.ts:105 - remark parses markdown to mdast; remark-gfm sets listItem.checked property
  2. MarkdownToAdfEngine.ts:109 - calls convertMdastToAdfSync(processedTree)
  3. MarkdownToAdfEngine.ts:346 - calls astBuilder.buildADFFromMdast(processedTree)
  4. ASTBuilder.ts:1209 - buildADFFromMdast() calls convertMdastNodesToADF(tree.children)
  5. ASTBuilder.ts:1237 - iterates nodes, dispatches to convertMdastNodeToADF(node)
  6. ASTBuilder.ts:1270-1271 - case 'list' dispatches to convertMdastList(node) [DECISION POINT: must check if children have checked property]
  7. ASTBuilder.ts:1630-1636 - convertMdastList() emits bulletList/orderedList [CHANGE: conditionally emit taskList with localId]
  8. ASTBuilder.ts:1274-1275 - case 'listItem' dispatches to convertMdastListItem(node) [DECISION POINT: must check node.checked]
  9. ASTBuilder.ts:1638-1643 - convertMdastListItem() emits listItem [CHANGE: conditionally emit taskItem with state and localId; inline content without paragraph wrapper]

Data Flow: Checkbox State (ADF to Markdown)

Entry Point: src/parser/engines/AdfToMarkdownEngine.ts:106 - convertAdfToMarkdown(adf)
Flow Path:

  1. AdfToMarkdownEngine.ts:290-301 - iterates top-level content nodes, looks up converter via registry.getNodeConverter(node.type)
  2. ConverterRegistry.ts:28-34 - looks up converter by type string
  3. [NEW] TaskListConverter.toMarkdown() - iterates taskItem children, delegates to taskItem converter
  4. [NEW] TaskItemConverter.toMarkdown() - reads attrs.state, prefixes with - [x] or - [ ]

Affected Interfaces (ALL must be updated):

  • src/types/adf.types.ts - Add TaskListNode and TaskItemNode interfaces
  • src/validators/schemas/adf-schema.json:36-39 - Add taskList, taskItem to node type enum
  • src/validators/schemas/adf-schema.json - Add validation rules for taskList/taskItem attrs
  • src/parser/markdown-to-adf/ASTBuilder.ts:1630-1636 - convertMdastList - detect task list
  • src/parser/markdown-to-adf/ASTBuilder.ts:1638-1643 - convertMdastListItem - detect task item
  • src/parser/markdown-to-adf/ASTBuilder.ts:234-255 - convertList (token path) - detect task list
  • src/parser/markdown-to-adf/ASTBuilder.ts:257-274 - convertListItem (token path) - detect task item
  • src/parser/markdown-to-adf/ASTBuilder.ts:2300-2440 - parseBlockContent - detect task lists in fence blocks
  • src/parser/markdown-to-adf/MarkdownTokenizer.ts:461-538 - parseList/parseListItem - detect checkbox syntax
  • src/parser/engines/AdfToMarkdownEngine.ts:212-255 - Register new converters
  • src/parser/Parser.ts:267-310 - Register new converters (legacy path)
  • [NEW] src/parser/adf-to-markdown/nodes/TaskListConverter.ts - New file
  • [NEW] src/parser/adf-to-markdown/nodes/TaskItemConverter.ts - New file

Critical Implementation Note: This is a cross-cutting change affecting 8+ files across both conversion directions. The mdast path is the primary code path; the token-based path is secondary but still active inside ADF fence block content parsing.

Affected Files

  • src/types/adf.types.ts - Add TaskListNode (with localId attr) and TaskItemNode (with localId, state attrs) interfaces
  • src/validators/schemas/adf-schema.json:36-39 - Add taskList, taskItem to node type enum; add conditional validation for attrs
  • src/parser/markdown-to-adf/ASTBuilder.ts:1630-1636 - convertMdastList: check if list children have checked property; if so emit taskList with localId
  • src/parser/markdown-to-adf/ASTBuilder.ts:1638-1643 - convertMdastListItem: check node.checked; emit taskItem with state: DONE/TODO and localId; content should be inline (not wrapped in paragraph per ADF spec)
  • src/parser/markdown-to-adf/ASTBuilder.ts:234-274 - Token-path convertList/convertListItem: detect checkbox markers in content, emit taskList/taskItem
  • src/parser/markdown-to-adf/ASTBuilder.ts:2404-2427 - parseBlockContent bullet list section: add checkbox detection
  • src/parser/markdown-to-adf/MarkdownTokenizer.ts:492-538 - parseListItem: detect [x]/[ ] pattern in first line content, store as metadata
  • src/parser/engines/AdfToMarkdownEngine.ts:212-241 - Register TaskListConverter and TaskItemConverter
  • src/parser/Parser.ts:269-296 - Register TaskListConverter and TaskItemConverter (legacy path)
  • [NEW] src/parser/adf-to-markdown/nodes/TaskListConverter.ts - Convert ADF taskList to markdown task list
  • [NEW] src/parser/adf-to-markdown/nodes/TaskItemConverter.ts - Convert ADF taskItem to - [x] or - [ ] markdown

Integration Points

  • ASTBuilder.convertMdastList depends on child node checked property (set by remark-gfm)
  • TaskListConverter delegates to TaskItemConverter via registry (same pattern as BulletListConverter -> ListItemConverter)
  • JSON schema validation gates ADF output -- must be updated first or validation will reject valid task list output
  • localId generation is new infrastructure -- crypto.randomUUID() is available in Node >= 19 (project requires 20.11.1+)

Medium Severity Risks

  • Token-based parser path may be missed: The parseBlockContent() method in ASTBuilder has its own bullet list parser (line 2404-2427) that does not use MarkdownTokenizer -- it must also handle checkbox syntax independently.
  • taskItem content structure differs from listItem: Per ADF spec, taskItem contains inline content directly (text nodes), NOT wrapped in a paragraph node. listItem wraps content in paragraph. The implementation must handle this structural difference.
  • ASTBuilder.convertAdfNodeToMdast (line 2003): The remark stringify path's mdast conversion does not handle taskList/taskItem -- falls to default case. This may need updating for full round-trip support, though it appears to be a less-used code path.

@acreeger

acreeger commented Feb 16, 2026

Copy link
Copy Markdown
Author

Implementation Plan for Issue #8

Summary

Markdown checkbox task lists (- [x]/- [ ]) are parsed by remark-gfm (already a dependency) which sets checked: true/false on mdast listItem nodes, but the ASTBuilder ignores this property and always emits bulletList/listItem ADF nodes. This plan adds full bidirectional support: detecting checkbox state in MD-to-ADF conversion (both mdast and token paths) and converting ADF taskList/taskItem back to markdown checkbox syntax.

Questions and Key Decisions

Question Answer Rationale
How should localId be generated? crypto.randomUUID() Project requires Node >= 20.11.1 which supports this natively. Atlassian docs confirm random IDs are acceptable.
Should mixed lists (some checked, some not) become taskList? Yes, if any item has checked !== null && checked !== undefined Per ADF spec, taskList supports both TODO and DONE states. remark-gfm sets checked to boolean on checkbox items, null/undefined on regular items.
What content structure should taskItem use? Inline nodes directly (no paragraph wrapper) Confirmed from official Atlaskit ADF schema -- taskItem.content is inline_node[], not block nodes like listItem.
Should the token-based parser path also support checkboxes? Yes The parseBlockContent() in ASTBuilder uses its own bullet list parser for ADF fence block content. Both paths must be updated.
How to represent checkbox state in the token system? Add a ListItemToken interface extending Token with checked?: boolean property The existing pattern (e.g., ListToken, FenceToken) extends Token for type-specific fields. A new ListItemToken follows this convention. See Step 1 details.
What happens when a checkbox item has multi-paragraph or nested-list content? Fall back to bulletList/listItem for that item; only simple inline-content items become taskItem ADF taskItem only allows inline content (text, emoji, mention, date, hardBreak, marks). Multi-paragraph items, nested sub-lists, or other block content cannot be represented in a taskItem. See "Edge Case: Complex Checkbox Items" section below.

Edge Case: Complex Checkbox Items

ADF taskItem content is restricted to inline nodes. Markdown checkbox items can contain block-level content:

- [x] Simple task (inline only -- converts to taskItem)
- [ ] Complex task

  Second paragraph of content (block-level -- CANNOT be taskItem)
- [ ] Task with sub-list
  - nested item (block-level -- CANNOT be taskItem)

Strategy -- per-item fallback:

  • When converting a checkbox list, inspect each listItem's children individually.
  • If a checkbox item contains ONLY a single paragraph (i.e., node.children.length === 1 && node.children[0].type === 'paragraph'), emit it as a taskItem with inline content extracted from that paragraph.
  • If a checkbox item contains multiple children (multiple paragraphs, nested lists, code blocks, etc.), fall back to emitting a regular listItem inside a bulletList for that specific item.
  • At the list level: If ALL checkbox items qualify for taskItem, emit the whole list as taskList. If ANY item requires fallback, emit the entire list as a bulletList to maintain valid ADF structure (since taskList can only contain taskItem children, not listItem).
  • This applies to both the mdast path (convertMdastList) and the token path (convertList).

High-Level Execution Phases

  1. Types and Schema (Step 1): Add ListItemToken to token types, TaskListNode/TaskItemNode to ADF types, update JSON schema validation
  2. MD-to-ADF mdast path (Step 2): Modify convertMdastList/convertMdastListItem to detect checkbox state with per-item inline content validation
  3. MD-to-ADF token path (Step 3): Update tokenizer checkbox detection and ASTBuilder token-path conversion with per-item validation
  4. ADF-to-MD reverse converters (Step 4): Create TaskListConverter/TaskItemConverter, register in engines
  5. Tests (Step 5): Unit tests for new converters + integration tests for round-trip conversion including edge cases

Quick Stats

  • 0 files for deletion
  • 9 files to modify (added src/parser/markdown-to-adf/types.ts)
  • 3 new files to create
  • Dependencies: None (crypto.randomUUID() is built-in Node API)
  • Estimated complexity: Complex

Potential Risks (HIGH/CRITICAL only)

  • ADF Schema validation will reject taskList/taskItem until schema is updated: The JSON schema's node type enum does not include taskList or taskItem. This must be updated in Step 1 or all subsequent test assertions on valid ADF output will fail.
  • taskItem content is inline-only: Unlike listItem which wraps content in paragraph, taskItem contains inline nodes directly. The implementation must extract inline content from mdast's paragraph wrapper during conversion, and fall back to bulletList when items contain non-inline content.

Complete Implementation Guide (click to expand for step-by-step details)

Automated Test Cases to Create

Test File: tests/unit/nodes/TaskListConverter.test.ts (NEW)

Purpose: Unit tests for ADF taskList to markdown conversion

Click to expand complete test structure (~60 lines)
// Follow exact pattern from BulletListConverter.test.ts
// Import TaskListConverter, mock taskItem converter via registry
describe('TaskListConverter', () => {
  describe('nodeType', () => {
    // should have nodeType 'taskList'
  })
  describe('toMarkdown', () => {
    // should convert single taskItem (DONE) -> '- [x] text'
    // should convert single taskItem (TODO) -> '- [ ] text'
    // should convert mixed states
    // should handle empty taskList -> ''
    // should handle undefined content -> ''
    // should filter out empty items
    // should handle missing taskItem converter (fallback)
    // should pass correct context (depth+1, parent) to taskItem converter
  })
})

Test File: tests/unit/nodes/TaskItemConverter.test.ts (NEW)

Purpose: Unit tests for ADF taskItem to markdown conversion

Click to expand complete test structure (~50 lines)
// Follow exact pattern from ListItemConverter.test.ts
// Import TaskItemConverter, mock paragraph/text converters
describe('TaskItemConverter', () => {
  describe('nodeType', () => {
    // should have nodeType 'taskItem'
  })
  describe('toMarkdown', () => {
    // should convert DONE state -> '- [x] content'
    // should convert TODO state -> '- [ ] content'
    // should handle inline content directly (text nodes, not paragraph-wrapped)
    // should handle multiple inline nodes (bold text, links, etc.)
    // should handle empty content -> '- [ ] ' or '- [x] '
    // should handle undefined content -> appropriate prefix only
    // should handle multi-line content with proper indentation
  })
})

Test File: tests/unit/parser/task-list-conversion.test.ts (NEW)

Purpose: Integration-style tests for MD-to-ADF task list conversion (both mdast and token paths)

Click to expand complete test structure (~100 lines)
// Uses Parser class directly to test end-to-end conversion
import { Parser } from '../../../src/parser/Parser';
describe('Task List Conversion', () => {
  describe('Markdown to ADF', () => {
    // should convert '- [x] Done' to taskList with taskItem state=DONE
    // should convert '- [ ] Todo' to taskList with taskItem state=TODO
    // should convert mixed checkbox list to single taskList with mixed states
    // should keep regular bullet list (no checkboxes) as bulletList
    // should handle task items with inline formatting (bold, links, code)
    // should generate localId attrs on taskList and each taskItem
    // taskItem content should be inline nodes, NOT wrapped in paragraph
  })
  describe('Edge Cases: Complex Checkbox Items', () => {
    // should fall back to bulletList when any checkbox item has multiple paragraphs
    // should fall back to bulletList when any checkbox item has nested sub-lists
    // should fall back to bulletList when any checkbox item has a code block
    // should still convert simple checkbox items correctly (single-paragraph only)
    // mixed list with both simple and complex items -> entire list becomes bulletList
  })
  describe('ADF to Markdown', () => {
    // should convert taskList/taskItem DONE -> '- [x] text'
    // should convert taskList/taskItem TODO -> '- [ ] text'
    // should convert mixed state taskList correctly
  })
  describe('Round-trip', () => {
    // MD -> ADF -> MD should preserve checkbox state
    // ADF -> MD -> ADF should preserve taskList/taskItem structure
  })
})

Files to Modify

1. src/parser/markdown-to-adf/types.ts:28-36,58-63

Change: Add ListItemToken interface with checked?: boolean property. This is the token-level representation of checkbox state, following the existing pattern of ListToken and FenceToken which extend Token with type-specific fields.

At lines 58-63, after the existing ListToken interface, add:

export interface ListItemToken extends Token {
  type: 'listItem';
  checked?: boolean; // true = [x], false = [ ], undefined = regular list item
}

The tokenizer (MarkdownTokenizer.parseListItem) will return ListItemToken instead of plain Token, and the ASTBuilder token-path methods (convertList, convertListItem) will cast to ListItemToken to read the checked property. No changes needed to TokenType enum since listItem is already a valid type.

2. src/types/adf.types.ts:77

Change: Add TaskListNode and TaskItemNode interfaces after ListItemNode (line 77)

// Add after line 77:
export interface TaskListNode extends ADFNode {
  type: 'taskList';
  attrs: { localId: string };
  content: TaskItemNode[];
}

export interface TaskItemNode extends ADFNode {
  type: 'taskItem';
  attrs: { localId: string; state: 'TODO' | 'DONE' };
  content?: ADFNode[];  // inline nodes directly, no paragraph wrapper
}

3. src/validators/schemas/adf-schema.json:36-39

Change: Add taskList and taskItem to the node type enum. Add conditional validation rules for their required attrs.

At line 36-39, add "taskList", "taskItem" to the enum array.

After the orderedList conditional block (line 131), add two new conditional blocks:

Click to expand schema additions (~30 lines)
// Add to allOf array after the orderedList block:
{
  "if": { "properties": { "type": { "const": "taskList" } } },
  "then": {
    "properties": {
      "attrs": {
        "type": "object",
        "required": ["localId"],
        "properties": {
          "localId": { "type": "string" }
        }
      }
    }
  }
},
{
  "if": { "properties": { "type": { "const": "taskItem" } } },
  "then": {
    "properties": {
      "attrs": {
        "type": "object",
        "required": ["localId", "state"],
        "properties": {
          "localId": { "type": "string" },
          "state": { "type": "string", "enum": ["TODO", "DONE"] }
        }
      }
    }
  }
}

4. src/parser/markdown-to-adf/ASTBuilder.ts:1630-1643

Change: Modify convertMdastList (line 1630) to detect task lists and emit taskList node, with per-item validation for inline-only content. Modify convertMdastListItem (line 1638) to detect checked items and emit taskItem node with inline content.

At convertMdastList (line 1630-1636):

  • Check if any child has checked !== null && checked !== undefined (this identifies it as a checkbox list)
  • If so, validate that ALL checkbox items have simple content (single paragraph child only)
  • If all items qualify: emit { type: 'taskList', attrs: { localId: crypto.randomUUID() }, content: ... } calling a new convertMdastTaskItem for each child
  • If any item has complex content (multiple paragraphs, nested lists): fall back to standard bulletList/listItem conversion for the entire list
  • Non-checkbox lists keep current behavior

At convertMdastListItem -- keep existing method as-is for non-checkbox items.

Add new private method convertMdastTaskItem(node: any): ADFNode:

  • Emit { type: 'taskItem', attrs: { localId: crypto.randomUUID(), state: node.checked ? 'DONE' : 'TODO' }, content: ... }
  • Critical: Extract inline children from the first paragraph child: node.children[0].children and convert each via the existing mdast inline conversion logic
  • Items where checked is null/undefined within an otherwise-checkbox list should use state: 'TODO' as default

Add new private helper isCheckboxItemSimple(node: any): boolean:

  • Returns true if node.children.length === 1 && node.children[0].type === 'paragraph'
  • Returns false for multi-paragraph items, items with nested lists, code blocks, etc.

5. src/parser/markdown-to-adf/MarkdownTokenizer.ts:492-538

Change: In parseListItem (line 492), after extracting content on line 501-502, detect checkbox syntax [x]/[ ] and store state in the token as ListItemToken.

After line 502 (const content = match?.[1] || firstLine;):

  • Check if content starts with [x] or [X] or [ ] using regex /^\[(x|X| )\]\s/
  • If matched: strip the checkbox prefix from content, set checked = (match === 'x' || match === 'X')
  • Store checked property on the returned token object at line 531-537

Return type should satisfy ListItemToken interface when checked is present:

return {
  type: 'listItem',
  content: lines.join('\n'),
  children,
  position: startPos,
  raw: raw.trimEnd(),
  ...(checked !== undefined && { checked })
} as ListItemToken;

6. src/parser/markdown-to-adf/ASTBuilder.ts:234-274

Change: Modify token-path convertList (line 234) and convertListItem (line 257) to handle checkbox tokens, with the same per-item inline content validation as the mdast path.

At convertList (line 234-255):

  • Import/cast children as ListItemToken from types.ts
  • Check if any child token has a checked property
  • If so, validate all checkbox items have simple content (same logic: single paragraph child or inline-only children)
  • If all qualify: emit taskList with localId attr; convert children via a new convertTaskItem method
  • If any item is complex: fall back to standard bulletList

At convertListItem (line 257-274) -- keep as-is for non-checkbox items.

Add new private method convertTaskItem(token: ListItemToken): ADFNode:

  • Emit taskItem with localId and state attrs based on token.checked
  • Content should be inline nodes extracted from children

7. src/parser/markdown-to-adf/ASTBuilder.ts:2404-2427

Change: In parseBlockContent bullet list section, add checkbox detection before emitting bulletList/listItem.

At line 2404-2427:

  • Before the current isBulletList check, add a check for checkbox patterns: /^\s*[-*+]\s+\[(x|X| )\]\s/
  • If any line matches checkbox pattern AND all items are simple (single-line, no continuation blocks), emit taskList/taskItem nodes instead
  • Parse [x]/[X] as DONE, [ ] as TODO
  • Extract content after the checkbox marker
  • Generate localId for taskList and each taskItem
  • Content of taskItem should be inline nodes (use parseInlineContentWithSocialElements)
  • If items are complex, fall back to bulletList as usual

8. src/parser/engines/AdfToMarkdownEngine.ts:12-36,214-241

Change: Import and register TaskListConverter and TaskItemConverter.

At imports (after line 18, the ListItemConverter import):

import { TaskListConverter } from '../adf-to-markdown/nodes/TaskListConverter.js';
import { TaskItemConverter } from '../adf-to-markdown/nodes/TaskItemConverter.js';

At registerConverters (line 214-241), add to the registerNodes array:

new TaskListConverter(),
new TaskItemConverter(),

9. src/parser/Parser.ts:29-31,269-296

Change: Import and register TaskListConverter and TaskItemConverter (legacy path).

At imports (after line 31, the ListItemConverter import):

import { TaskListConverter } from './adf-to-markdown/nodes/TaskListConverter.js';
import { TaskItemConverter } from './adf-to-markdown/nodes/TaskItemConverter.js';

At registerConverters (line 269-296), add to the registerNodes array:

new TaskListConverter(),
new TaskItemConverter(),

New Files to Create

src/parser/adf-to-markdown/nodes/TaskListConverter.ts (NEW)

Purpose: Convert ADF taskList node to markdown task list syntax. Follows same pattern as BulletListConverter.

Click to expand complete structure (~30 lines)
// Follow BulletListConverter pattern exactly
// nodeType = 'taskList'
// toMarkdown: iterate content, delegate each to 'taskItem' converter via registry
// Filter empty items, join with '\n'
// Pass context with depth+1 and parent=taskListNode

src/parser/adf-to-markdown/nodes/TaskItemConverter.ts (NEW)

Purpose: Convert ADF taskItem node to markdown checkbox syntax (- [x] or - [ ] ).

Click to expand complete structure (~40 lines)
// Follow ListItemConverter pattern with key differences:
// nodeType = 'taskItem'
// toMarkdown:
//   1. Determine prefix: node.attrs.state === 'DONE' ? '- [x] ' : '- [ ] '
//   2. Convert inline content (taskItem has inline nodes directly, not paragraph-wrapped)
//      - Iterate node.content, for each child:
//        - Look up converter via registry (text, mention, emoji, etc.)
//        - Convert and concatenate
//   3. Handle multi-line: indent continuation lines with 6 spaces (to align with content after '- [x] ')
//   4. Return prefixed content

tests/unit/parser/task-list-conversion.test.ts (NEW)

Purpose: End-to-end tests for task list conversion in both directions, including edge case coverage for complex checkbox items. See test structure above.

Detailed Execution Order

Step 1: Types, Schema, and Token Interface

Files: src/parser/markdown-to-adf/types.ts, src/types/adf.types.ts, src/validators/schemas/adf-schema.json

  1. Add ListItemToken interface to src/parser/markdown-to-adf/types.ts after the ListToken interface (after line 63) with checked?: boolean property -> Verify: types compile
  2. Add TaskListNode and TaskItemNode interfaces to src/types/adf.types.ts after line 77 -> Verify: types compile, exported from src/types/index.ts (already re-exports all from adf.types)
  3. Add taskList, taskItem to enum in src/validators/schemas/adf-schema.json line 36-39 -> Verify: enum includes new types
  4. Add conditional validation blocks for taskList attrs (localId) and taskItem attrs (localId, state) in the allOf array after line 131 -> Verify: schema validates correctly

Step 2: MD-to-ADF conversion (mdast path)

Files: src/parser/markdown-to-adf/ASTBuilder.ts (lines 1630-1643)

  1. Add isCheckboxItemSimple(node) helper method that returns true only if item has a single paragraph child -> Verify: correctly identifies simple vs complex items
  2. Modify convertMdastList at line 1630 to detect task lists via child checked property, validate all items are simple, emit taskList with localId or fall back to bulletList -> Verify: simple checkbox markdown produces taskList; complex checkbox markdown falls back to bulletList
  3. Add convertMdastTaskItem method to emit taskItem with state/localId, extract inline content from paragraph wrapper -> Verify: each item has correct state and inline-only content

Step 3: MD-to-ADF conversion (token path)

Files: src/parser/markdown-to-adf/MarkdownTokenizer.ts (lines 492-538), src/parser/markdown-to-adf/ASTBuilder.ts (lines 234-274, 2404-2427)

  1. Add checkbox detection in MarkdownTokenizer.parseListItem after line 502, return ListItemToken with checked property -> Verify: tokens have checked property
  2. Add convertTaskItem method to ASTBuilder for token path -> Verify: method compiles
  3. Modify ASTBuilder.convertList at line 234 to check for checkbox tokens, validate simplicity, emit taskList or fall back -> Verify: token path emits taskList for simple items, bulletList for complex
  4. Modify ASTBuilder.parseBlockContent at line 2404 to detect checkbox patterns in fence block content with same validation -> Verify: checkboxes inside ADF fence blocks produce taskList when simple

Step 4: ADF-to-MD reverse converters and registration

Files: src/parser/adf-to-markdown/nodes/TaskListConverter.ts (NEW), src/parser/adf-to-markdown/nodes/TaskItemConverter.ts (NEW), src/parser/engines/AdfToMarkdownEngine.ts, src/parser/Parser.ts

  1. Create TaskListConverter.ts following BulletListConverter pattern -> Verify: file compiles
  2. Create TaskItemConverter.ts following ListItemConverter pattern with checkbox prefix -> Verify: file compiles
  3. Import and register both converters in AdfToMarkdownEngine.ts at line 214-241 -> Verify: registry recognizes taskList/taskItem
  4. Import and register both converters in Parser.ts at line 269-296 -> Verify: legacy path also registers converters

Step 5: Tests

Files: tests/unit/nodes/TaskListConverter.test.ts (NEW), tests/unit/nodes/TaskItemConverter.test.ts (NEW), tests/unit/parser/task-list-conversion.test.ts (NEW)

  1. Create TaskListConverter.test.ts following BulletListConverter.test.ts pattern -> Verify: all tests pass
  2. Create TaskItemConverter.test.ts following ListItemConverter.test.ts pattern -> Verify: all tests pass
  3. Create task-list-conversion.test.ts with end-to-end MD->ADF->MD tests including edge case tests for multi-paragraph and nested-list checkbox items -> Verify: round-trip preserves checkbox state, complex items fall back correctly
  4. Run full test suite -> Verify: no existing tests broken

Execution Plan

1. Run Step 1 (sequential) -- Types, Schema, and Token Interface
   Foundation step: all other steps import these types.
   Files: types.ts, adf.types.ts, adf-schema.json

2. Run Step 2, Step 3, Step 4 in parallel -- three independent implementation tracks:
   - Step 2: MD-to-ADF mdast path (ASTBuilder.ts lines 1630-1643 only)
   - Step 3: MD-to-ADF token path (MarkdownTokenizer.ts + ASTBuilder.ts lines 234-274, 2404-2427)
   - Step 4: ADF-to-MD reverse converters (NEW files + registration in AdfToMarkdownEngine.ts, Parser.ts)
   These touch different files/methods with no overlap.

3. Run Step 5 (sequential) -- Tests
   Must run after Steps 2-4 complete since tests exercise all implementation paths.
   Files: 3 new test files

Dependencies and Configuration

None. crypto.randomUUID() is a built-in Node.js API available in the project's minimum Node version (20.11.1). No new packages needed.

@acreeger

acreeger commented Feb 16, 2026

Copy link
Copy Markdown
Author

Implementation Complete

Summary

Added full bidirectional support for markdown checkbox task lists (- [x]/- [ ]) converting to/from ADF taskList/taskItem nodes. The implementation covers all three parsing paths (mdast, token, and block content) plus reverse ADF-to-markdown conversion.

Changes Made

  • src/types/adf.types.ts: Added TaskListNode and TaskItemNode interfaces
  • src/validators/schemas/adf-schema.json: Added taskList/taskItem to node type enum with conditional validation for localId and state attrs
  • src/parser/markdown-to-adf/types.ts: Added ListItemToken interface with checked property
  • src/parser/markdown-to-adf/ASTBuilder.ts: Modified convertMdastList/convertMdastListItem for mdast path; added convertList/convertListItem checkbox detection for token path; added checkbox detection in parseBlockContent for fence blocks
  • src/parser/markdown-to-adf/MarkdownTokenizer.ts: Added checkbox syntax detection in parseListItem
  • src/parser/adf-to-markdown/nodes/TaskListConverter.ts: New converter for ADF taskList to markdown
  • src/parser/adf-to-markdown/nodes/TaskItemConverter.ts: New converter for ADF taskItem to markdown checkbox
  • src/parser/engines/AdfToMarkdownEngine.ts: Registered new converters
  • src/parser/Parser.ts: Registered new converters (legacy path)

Validation Results

  • Tests: 570 passed (all runnable tests green, 35 suites with pre-existing ESM import failures unchanged)
  • Typecheck: Passed (zero errors)
  • New tests: 19 tests across 3 files (2 unit test suites pass, 1 integration suite has pre-existing ESM issue)

Detailed Changes by File (click to expand)

src/types/adf.types.ts

Changes: Added type definitions for task list ADF nodes

  • Added TaskListNode interface with localId attr and TaskItemNode[] content
  • Added TaskItemNode interface with localId and state (TODO/DONE) attrs

src/validators/schemas/adf-schema.json

Changes: Schema validation for new node types

  • Added taskList and taskItem to node type enum
  • Added conditional validation: taskList requires localId, taskItem requires localId + state

src/parser/markdown-to-adf/types.ts

Changes: Token interface for checkbox state

  • Added ListItemToken extending Token with optional checked: boolean

src/parser/markdown-to-adf/ASTBuilder.ts

Changes: Three parsing paths updated for checkbox detection

  • convertMdastList: Detects checkbox items via checked property, emits taskList with localId
  • convertMdastTaskItem: New method extracting inline content from paragraph wrapper
  • convertList (token path): Detects checked tokens, emits taskList
  • convertTaskItem (token path): New method for token-based task item conversion
  • parseBlockContent: Added checkbox pattern detection before bullet list handling
  • Edge case: Falls back to bulletList if any item has complex content (multi-paragraph/nested lists)

src/parser/markdown-to-adf/MarkdownTokenizer.ts

Changes: Checkbox syntax detection in tokenizer

  • parseListItem: Detects [x]/[ ] prefix, strips it from content, stores checked on token

src/parser/adf-to-markdown/nodes/TaskListConverter.ts (NEW)

Changes: ADF-to-markdown converter for task lists

  • Follows BulletListConverter pattern
  • Iterates content, delegates to taskItem converter via registry

src/parser/adf-to-markdown/nodes/TaskItemConverter.ts (NEW)

Changes: ADF-to-markdown converter for task items

  • Prefixes with - [x] (DONE) or - [ ] (TODO)
  • Handles inline content directly (not paragraph-wrapped)
  • 6-space indentation for multi-line content

src/parser/engines/AdfToMarkdownEngine.ts

Changes: Converter registration

  • Imported and registered TaskListConverter and TaskItemConverter

src/parser/Parser.ts

Changes: Legacy converter registration

  • Imported and registered TaskListConverter and TaskItemConverter

Test Files (3 NEW)

  • tests/unit/nodes/TaskListConverter.test.ts: 9 unit tests
  • tests/unit/nodes/TaskItemConverter.test.ts: 10 unit tests
  • tests/unit/parser/task-list-conversion.test.ts: Integration tests (affected by pre-existing ESM issue)

…ox syntax

Convert markdown checkbox task lists (- [x]/- [ ]) to ADF taskList/taskItem
nodes with proper state (DONE/TODO) and localId attributes, instead of
stripping checkbox state and emitting plain bulletList/listItem nodes.

Covers all three parsing paths (mdast, token, block content) plus
ADF-to-markdown reverse conversion via new TaskListConverter and
TaskItemConverter. Falls back to bulletList for complex items with
nested content that can't be represented as inline-only taskItem nodes.

Fixes JeromeErasmus#8
@acreeger
acreeger force-pushed the fix/issue-8__checkbox-task-list branch from 4800bf0 to 78bb230 Compare February 16, 2026 07:09
…Erasmus#8

- Add task list and task item node types to Markdown lexer
- Implement ASTBuilder conversion between checkbox syntax and task nodes
- Add unit tests for task list parsing and AST building
- Support bidirectional transformation of checkbox markdown to ADF
@acreeger
acreeger marked this pull request as ready for review February 16, 2026 07:28
@acreeger

Copy link
Copy Markdown
Author

iloom Session Summary

Key Themes:

  • Checkbox task lists required coordinated changes across three independent parsing paths (mdast, token-based, and raw regex in fence blocks)
  • ADF taskItem nodes have fundamentally different content structure than listItem (inline-only vs paragraph-wrapped), requiring special extraction logic
  • The existing remark-gfm dependency already provides full checkbox parsing—the gap was in ASTBuilder ignoring the checked property

Session Details (click to expand)

Key Insights

  • Three parallel list parsing paths exist: The mdast path (via remark-gfm) is primary, but the token-based path (MarkdownTokenizer) is used for general markdown, and a third raw regex path (parseBlockContent) handles content inside ADF fence blocks. All three had to be updated independently.

  • remark-gfm already parses checkboxes: The mdast ListItem type has a checked: boolean | null | undefined property set by micromark-extension-gfm-task-list-item. No new parsing dependencies were needed—ASTBuilder was simply ignoring this property.

  • ADF schema validation is a hard requirement: The JSON schema at adf-schema.json has a strict enum of allowed node types. Adding taskList/taskItem to the codebase without updating the schema causes all validation to fail silently downstream.

  • Content structure differs between listItem and taskItem: ADF listItem wraps content in paragraph nodes, but taskItem contains inline nodes directly (per Atlassian's ADF spec). This means mdast list items must have their inline children extracted from paragraph wrappers during conversion.

  • The token path destructively strips checkbox syntax: MarkdownTokenizer.parseListItem uses regex to extract content, stripping [x] prefixes. When falling back to bulletList for complex items, the checkbox text must be re-prepended to avoid data loss.

Decisions Made

  • Fallback strategy for complex items: If any checkbox list item contains multiple paragraphs or nested sub-lists, the entire list falls back to bulletList/listItem because ADF taskList can only contain taskItem children (which only support inline content). This is per-item validation—one complex item downgrades the whole list.

  • Token type architecture: Added ListItemToken interface extending Token with optional checked?: boolean property, following the existing pattern of ListToken and FenceToken. This avoids creating new token types and keeps checkbox state as metadata.

  • localId generation: Use crypto.randomUUID() for all localId attributes on taskList and taskItem nodes. The project requires Node >= 20.11.1 which supports this natively, and Atlassian docs confirm random IDs are acceptable (deterministic hashing is unnecessary).

  • Content property consistency: Always include content on taskItem nodes (even as empty array []) rather than conditionally omitting it via spread operators. This aligns the mdast and token conversion paths and avoids downstream bugs where consumers check node.content vs node.content?.length.

Challenges Resolved

  • ESM import failures in tests: Initial tests failed with SyntaxError: Unexpected token 'export' when importing unified/remark packages. This was because tests were run with npx jest instead of the configured node --experimental-vm-modules node_modules/.bin/jest. The mock workarounds were unnecessary—using the correct test command made all ESM imports work natively.

  • Missing MD-to-ADF test coverage: Initial test suite only covered ADF-to-Markdown converters (the toMarkdown methods). The core feature—detecting checkbox syntax and producing taskList/taskItem nodes—had zero working tests. Added 26 unit tests covering both the mdast path (convertMdastList/convertMdastTaskItem) and token path (convertList/convertTaskItem), plus checkbox detection in MarkdownTokenizer.

  • Data loss on bulletList fallback: When complex checkbox items triggered fallback to bulletList, the token path had already stripped [x] from the content string. Fixed by re-prepending checkbox syntax in convertList when falling back, before passing tokens to convertListItem.

  • Inconsistent empty-item handling: isTokenItemSimple treated items with zero children as "simple" (allowing taskItem conversion), while isCheckboxItemSimple required exactly one paragraph child. Aligned both methods to accept zero children as simple, creating consistent behavior across parsing paths.

Lessons Learned

  • ASTBuilder has mixed concerns: The 2578-line ASTBuilder.ts file handles 15+ node types and contains both mdast-to-ADF and token-to-ADF conversion logic. Changes to list handling required modifying four separate methods (convertMdastList, convertMdastListItem, convertList, convertListItem) plus the regex-based parseBlockContent fallback path.

  • Converter registration happens in two places: Both AdfToMarkdownEngine and Parser have separate registerConverters methods that must register new converters. The Parser registration is for legacy API compatibility, but both are actively used.

  • Regex path in parseBlockContent is independent: The parseBlockContent method (lines ~2404-2427 in ASTBuilder) has its own bullet list parsing logic using direct regex matching, separate from both the mdast and token paths. It handles markdown content inside ADF fence blocks and requires its own checkbox detection pattern.

  • Case-insensitive checkbox detection required: Both [x] and [X] (uppercase) are valid checkbox syntax in GFM. The tokenizer regex must handle both via case-insensitive matching (/\[x\]/i), and the mdast path gets this for free from remark-gfm.


Generated with 🤖❤️ by iloom.ai

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Checkbox task lists (- [x]/- [ ]) lose check state — converted to plain bulletList instead of taskList

1 participant