Skip to content

✨ Add gfm-like2 preset with task lists, alerts, and single-tilde strikethrough#388

Merged
chrisjsewell merged 5 commits intomasterfrom
gfm-like2
May 6, 2026
Merged

✨ Add gfm-like2 preset with task lists, alerts, and single-tilde strikethrough#388
chrisjsewell merged 5 commits intomasterfrom
gfm-like2

Conversation

@chrisjsewell
Copy link
Copy Markdown
Member

@chrisjsewell chrisjsewell commented May 3, 2026

Summary

Adds a new gfm-like2 preset that extends gfm-like with three GFM features:

  • Task lists- [x] done / - [ ] todo checkbox syntax in list items
  • Alerts> [!NOTE], > [!TIP], > [!WARNING], etc. inside blockquotes
  • Single-tilde strikethrough~text~ in addition to ~~text~~

These are enabled via the gfm-like2 preset or individually through the tasklists, alerts, and strikethrough_single_tilde options. The existing gfm-like preset is unchanged, so as to remain back-compatible.

Why in markdown-it-py, not mdit-py-plugins?

Task lists and alerts are implemented by integrating detection directly into the existing block-level parsers (list.py and blockquote.py), rather than as post-processing rules:

  • Checkbox detection happens during list item parsing, before the sub-parser runs on the item content
  • Alert detection happens during blockquote parsing, before the inner content is tokenized

This design is not achievable from a plugin: plugins can only add new rules or post-process the token stream — they cannot modify the internals of list_block() or blockquote() to inject detection at the right point in the parsing pipeline. Implementing these as post-processing core rules would work functionally, but it means re-walking and mutating the token stream after the fact, which is less clean and less consistent with how the block parsers are designed to work.

Single-tilde strikethrough similarly extends the existing strikethrough rule's matching logic (opener/closer width matching), which is more naturally done inside the rule than bolted on externally.

Changes

  • rules_block/list.py — Detect [ ]/[x]/[X] at content start during list item parsing; set token.meta["checked"]; advance bMarks past the checkbox; add CSS classes (task-list-item, contains-task-list) after the list loop
  • rules_block/blockquote.py — Detect [!TYPE] on the first content line; emit alert_open/alert_close + title tokens instead of blockquote_open/blockquote_close; skip the marker line during tokenization
  • rules_inline/strikethrough.py — When strikethrough_single_tilde is enabled, accept 1 or 2 tildes (reject 3+); enforce opener/closer width matching in _postProcess
  • renderer.py — Add list_item_open render method that injects checkbox HTML when meta["checked"] is present
  • presets/__init__.py — Add gfm_like2 preset class
  • main.py — Register gfm-like2 in _PRESETS
  • utils.py — Add tasklists, alerts, strikethrough_single_tilde keys to OptionsType
  • pyproject.toml — Add pytest-timeout to test deps; set 10s default timeout
  • Test fixtures — 11 tasklist cases, 15 alert cases, 13 single-tilde strikethrough cases

Usage

from markdown_it import MarkdownIt

md = MarkdownIt("gfm-like2")
md.render("- [x] done\n- [ ] todo")
md.render("> [!NOTE]\n> This is a note.")
md.render("~strikethrough~")

…rikethrough

### Summary

Adds a new `gfm-like2` preset that extends `gfm-like` with three GFM features:

- **Task lists** — `- [x] done` / `- [ ] todo` checkbox syntax in list items
- **Alerts** — `> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, etc. inside blockquotes
- **Single-tilde strikethrough** — `~text~` in addition to `~~text~~`

These are enabled via the `gfm-like2` preset or individually through the `tasklists`, `alerts`, and `strikethrough_single_tilde` options.
The existing `gfm-like` preset is unchanged, so as to remain back-compatible.

### Why in markdown-it-py, not mdit-py-plugins?

Task lists and alerts are implemented by integrating detection directly into the existing block-level parsers (list.py and blockquote.py), rather than as post-processing rules. This mirrors [the approach taken in ubc_parser_md](https://github.com/useblocks/ubcode/tree/main/rust/ubc_parser_md) (the Rust markdown-it port), where:

- Checkbox detection happens during list item parsing, before the sub-parser runs on the item content
- Alert detection happens during blockquote parsing, before the inner content is tokenized

This design is not achievable from a plugin: plugins can only add new rules or post-process the token stream — they cannot modify the internals of `list_block()` or `blockquote()` to inject detection at the right point in the parsing pipeline. Implementing these as post-processing core rules would work functionally, but it means re-walking and mutating the token stream after the fact, which is less clean and less consistent with how the block parsers are designed to work.

Single-tilde strikethrough similarly extends the existing strikethrough rule's matching logic (opener/closer width matching), which is more naturally done inside the rule than bolted on externally.

### Changes

- **`rules_block/list.py`** — Detect `[ ]`/`[x]`/`[X]` at content start during list item parsing; set `token.meta["checked"]`; advance `bMarks` past the checkbox; add CSS classes (`task-list-item`, `contains-task-list`) after the list loop
- **`rules_block/blockquote.py`** — Detect `[!TYPE]` on the first content line; emit `alert_open`/`alert_close` + title tokens instead of `blockquote_open`/`blockquote_close`; skip the marker line during tokenization
- **`rules_inline/strikethrough.py`** — When `strikethrough_single_tilde` is enabled, accept 1 or 2 tildes (reject 3+); enforce opener/closer width matching in `_postProcess`
- **renderer.py** — Add `list_item_open` render method that injects checkbox HTML when `meta["checked"]` is present
- **`presets/__init__.py`** — Add `gfm_like2` preset class
- **`main.py`** — Register `gfm-like2` in `_PRESETS`
- **utils.py** — Add `tasklists`, `alerts`, `strikethrough_single_tilde` keys to `OptionsType`
- **pyproject.toml** — Add `pytest-timeout` to test deps; set 10s default timeout
- **Test fixtures** — 11 tasklist cases, 15 alert cases, 13 single-tilde strikethrough cases

### Usage

```python
from markdown_it import MarkdownIt

md = MarkdownIt("gfm-like2")
md.render("- [x] done\n- [ ] todo")
md.render("> [!NOTE]\n> This is a note.")
md.render("~strikethrough~")
```
@codecov
Copy link
Copy Markdown

codecov Bot commented May 3, 2026

Codecov Report

❌ Patch coverage is 96.50350% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.84%. Comparing base (df6fd36) to head (6167043).

Files with missing lines Patch % Lines
markdown_it/rules_block/blockquote.py 95.74% 2 Missing ⚠️
markdown_it/rules_block/list.py 94.73% 2 Missing ⚠️
markdown_it/rules_inline/strikethrough.py 96.87% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           master     #388    +/-   ##
========================================
  Coverage   95.84%   95.84%            
========================================
  Files          64       64            
  Lines        3487     3611   +124     
========================================
+ Hits         3342     3461   +119     
- Misses        145      150     +5     
Flag Coverage Δ
pytests 95.84% <96.50%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chrisjsewell
Copy link
Copy Markdown
Member Author

Overview

This PR adds a new gfm-like2 preset that extends gfm-like with three GitHub Flavored Markdown features: task lists, alerts, and single-tilde strikethrough. CI is passing. The change is well-structured with 12 files changed (+664/−22) and thorough test fixtures.

Strengths

  • Sound architectural rationale — The PR description clearly explains why these features belong in core rather than mdit-py-plugins: checkbox/alert detection needs to hook into list_block() and blockquote() internals during parsing, not as post-processing.
  • Backward compatible — The existing gfm-like preset is untouched; all new behavior is opt-in via gfm-like2 or individual option flags.
  • Good test coverage — 39 fixture cases covering happy paths, edge cases (invalid syntax, case insensitivity, nesting, mixed items), and negative cases.
  • Clean state management in list.pybMarks is saved/restored properly around the checkbox advancement.

Issues & Suggestions

1. Checkbox HTML is not disabled (Medium)

The rendered <input type="checkbox"> is interactive by default. GitHub renders these as disabled to prevent user interaction in static markdown. Consider adding disabled="" to the checkbox:

result += (
    '<input class="task-list-item-checkbox"'
    f' type="checkbox" disabled=""{checked_attr}> '
)

2. Alert: contentStart < nextLine guard may skip state.line assignment (Medium)

In blockquote.py, when contentStart >= nextLine (empty alert body), the code sets state.line = nextLine in the else branch. But when contentStart < nextLine, state.md.block.tokenize() is called — verify that tokenize() always updates state.line to nextLine. If not, this could leave state.line in an inconsistent state. The existing blockquote code path relies on tokenize doing this, so it's likely fine, but worth a comment.

3. Alert detection only checks startLine, not the first content line (Low)

_detect_alert is called with startLine, which should be the first line of blockquote content (after > stripping). The guard nextLine > startLine ensures at least one line was parsed. This looks correct, but the variable naming (startLine being both the blockquote start and the content line after prefix stripping) is confusing — a comment clarifying this would help.

4. _detect_task_checkbox requires trailing whitespace, but checkboxLen handles missing space (Low)

The function requires pos + 4 > maximum to fail, meaning it needs at least [ ] with a space. But checkboxLen conditionally adds the 4th char. Since _detect_task_checkbox already enforces the space exists, checkboxLen will always be 4 in practice. The conditional at lines checking contentStart + 3 is dead code — consider simplifying to always set checkboxLen = 4.

5. Strikethrough: 3+ tildes silently consumed as text (Low)

When single_tilde is enabled and ≥3 tildes are encountered, they're pushed as a plain text token and True is returned. This means the strikethrough rule "claims" the tildes, preventing any other inline rule from processing them. This matches GitHub behavior but is worth documenting as intentional.

6. Missing disabled attribute in test fixtures

If you add disabled="" per suggestion #1, all tasklist fixture expected outputs would need updating.

7. Preset naming

gfm-like2 is functional but not very descriptive. Consider gfm-like-extended or gfm-like-full? This is subjective — the current name works if you prefer brevity.

Summary

This is a well-implemented, well-tested PR with a clear rationale. The main actionable item is adding disabled to checkbox inputs (#1). The rest are minor cleanups. The approach of integrating into existing block parsers rather than post-processing is the right call architecturally.

@chrisjsewell
Copy link
Copy Markdown
Member Author

Changes Since Last Review

The PR now has 3 commits, 14 files changed (+680/−24). The new commits address the previous review feedback:

✅ Previously Raised Issues — Now Resolved

  1. Checkbox disabled attribute — Fixed. Checkboxes now render with disabled="" by default, controlled by a new tasklists_enabled option that omits it when True. All test fixtures updated accordingly.

  2. startLine comment clarity in blockquote.py — Fixed. A clear comment now explains that startLine refers to the first content line after > prefix stripping.

  3. tokenize() / state.line contract comment — Fixed. An inline comment now documents that tokenize() updates state.line to nextLine.

  4. Dead code in checkboxLen — Fixed. The conditional logic is gone; checkboxLen is now always set to 4, with a comment explaining _detect_task_checkbox already guarantees trailing whitespace.

  5. 3+ tildes documentation — Fixed. The docstring and inline comment now explicitly note this is intentional GitHub-matching behavior.

✅ New: Documentation Updates (commit 3)

  • docs/plugins.md — Lists task lists and alerts as embedded plugins; shows gfm-like vs gfm-like2 usage side by side.
  • docs/using.md — Documents the gfm-like2 preset in the configuration section.
  • utils.py — Added tasklists_enabled to OptionsType with docstring.

Remaining Minor Items

1. tasklists_enabled naming (Nit)

The option name tasklists_enabled reads like "are task lists enabled" rather than "are checkboxes interactive." Something like tasklists_interactive or tasklists_editable would be clearer. This is very minor — the docstring does explain it.

2. tasklists_enabled not set in gfm-like2 preset

The gfm_like2 preset sets tasklists=True but doesn't set tasklists_enabled. This means the default is False (disabled/readonly checkboxes), which matches GitHub's static rendering. This is correct behavior, just noting it's implicit via options.get("tasklists_enabled", False) in the renderer rather than explicit in the preset.

3. No test for tasklists_enabled=True path

There's no fixture test verifying that setting tasklists_enabled=True produces checkboxes without disabled="". A single test case would cover this code path.

4. Alert lines variable unused in alert branch

In blockquote.py, the alert branch does token.map = lines = [startLine, 0], but lines[1] is never updated in the alert path (unlike the regular blockquote path where lines[1] = nextLine). This means alert_open.map will always be [startLine, 0] — likely a bug. The regular blockquote path updates lines[1] later. You should add lines[1] = nextLine before alert_close.


Verdict

The review feedback was thoroughly addressed. The PR is in good shape. Item #4 (alert map not updated) is the only functional issue I'd flag as needing a fix before merge. The rest are minor polish items.

Rename `tasklists_enabled` to `tasklists_editable` for clarity and
set it explicitly to `False` in the `gfm-like2` preset.

Add focused tests for editable task list checkboxes and alert token
line maps, and make the alert container map handling explicit in
`blockquote.py`.
@chrisjsewell chrisjsewell merged commit 693bb24 into master May 6, 2026
15 checks passed
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.

1 participant