Skip to content

Add GFM alerts extension#420

Open
ia3andy wants to merge 3 commits intocommonmark:mainfrom
ia3andy:gfm-alerts
Open

Add GFM alerts extension#420
ia3andy wants to merge 3 commits intocommonmark:mainfrom
ia3andy:gfm-alerts

Conversation

@ia3andy
Copy link
Copy Markdown
Contributor

@ia3andy ia3andy commented Mar 3, 2026

Note

I have used Claude to code most of it, I am currently reviewing the generated code which seems to be overall valid. I've requested to follow the pattern of existing extensions (and compare to other implementations of this feature).

Summary

  • Adds new extension module for GitHub Flavored Markdown alerts
  • Implements alert detection as a PostProcessor (transforms BlockQuote nodes into Alert nodes after parsing)
  • Includes HTML and Markdown renderers
  • Supports all five standard GFM alert types: NOTE, TIP, IMPORTANT, WARNING, CAUTION
  • Case-insensitive type matching ([!note], [!Note] work like [!NOTE])
  • Supports custom alert types and title overrides via builder API (e.g., for localization)
  • Empty/whitespace-only alerts render as normal blockquotes (matching GitHub behavior)
  • Alerts only at document level (not inside list items or nested blockquotes, matching GitHub)

Test approach

  • AlertsSpecTest — parameterized spec-based tests from alerts-spec.txt, expectations verified against GitHub Markdown API (gh api markdown -f mode=gfm)
  • AlertsTest — custom types, builder validation, AST assertions
  • AlertsMarkdownRendererTest — roundtrip rendering tests

Changes from review

  • Rewrote from custom BlockParser to PostProcessor approach
  • Added case-insensitive type matching
  • Allow overriding standard type titles
  • Removed unnecessary TextContentRenderer (core handles it)
  • Constructor injection for Alert (immutable type, no public setter)
  • Moved STANDARD_TYPES from Alert node to AlertsExtension
  • Removed getCustomTypes() config leak from public API
  • Fixed visitor traversal (early return after successful conversion)
  • Removed dead code (capitalize fallback)

Closes #327

@ia3andy ia3andy marked this pull request as draft March 3, 2026 16:05
@ia3andy ia3andy marked this pull request as ready for review March 3, 2026 16:37
@ia3andy
Copy link
Copy Markdown
Contributor Author

ia3andy commented Mar 3, 2026

I've been through the code which seems good to me so I can say I am taking responsibilities for it like my own. Still, another set of eyes would be good :)

@ia3andy
Copy link
Copy Markdown
Contributor Author

ia3andy commented Mar 21, 2026

@robinst any thought ?

Copy link
Copy Markdown
Collaborator

@robinst robinst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some first comments, mostly around parsing and edge cases.

I was curious how GitHub implements this in cmark-gfm, and the answer seems to be that they don't:

github/cmark-gfm#350 (comment)

Alerts are not implemented in this project. They don't relate to their parser. They don't relate to syntax. So they're not here.

They are implemented as a post processing step on a sort of "DOM" version of the resulting document.

Can you explore what the implementation would look like as a PostProcessor instead? That has the advantage of not having to copy all the parsing logic from block quotes. (From the edge cases with nesting I commented about, it seems like they only look at top-level nodes.)

@ia3andy
Copy link
Copy Markdown
Contributor Author

ia3andy commented Mar 25, 2026

Thanks for the review! That all makes sense, will have a look soonish :)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ia3andy
Copy link
Copy Markdown
Contributor Author

ia3andy commented Mar 27, 2026

I made all the changes, I am currently generating a small jbang script to generate the spec from gh api result, not needed but cool :)

Add a jbang script (generate-alerts-spec.java) that generates
alerts-spec.txt from a template (alerts-spec-template.md) by rendering
each example through the GitHub Markdown API and normalizing the HTML.

Configure AlertsSpecTest with softbreak("<br>") to match GitHub's
rendering, reducing the normalizations needed in the generator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ia3andy
Copy link
Copy Markdown
Contributor Author

ia3andy commented Mar 27, 2026

@robinst it is ready for another review thanks!

Copy link
Copy Markdown
Collaborator

@robinst robinst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More feedback. Getting closer, but I think we can still simplify.

Neat idea to generate the test data by using the gh CLI.

- Replace AbstractVisitor with direct Document child iteration (only
  top-level block quotes are alerts, no need to walk the full tree)
- Remove unnecessary isContentWhitespaceOnly method (the parser never
  produces content nodes for whitespace-only block quote continuations)
- Remove redundant markerText/literal variables, use textNode.getLiteral()
- Restructure afterMarker checks to avoid 3x instanceof repetition
- Add Locale.ROOT to toUpperCase() calls (Turkish locale safety)
- Use var throughout all main source files
- Use single get()+null check instead of containsKey()+get() in renderer
- Add proper Node import in AlertNodeRenderer, remove FQN references
- LinkedHashMap → HashMap in AlertsExtension (ordering not needed)
- Update README: fix outdated custom type docs, align examples with
  AlertsExample.java
- Add screenshots showing rendered alerts with and without icons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ia3andy
Copy link
Copy Markdown
Contributor Author

ia3andy commented Mar 30, 2026

All review feedback has been addressed:

PostProcessor simplification:

  • Replaced AbstractVisitor with direct iteration of Document children
  • Removed isContentWhitespaceOnly (parser never produces content nodes for whitespace-only continuations)
  • Removed redundant markerText/literal variables
  • Restructured afterMarker checks to avoid repeated instanceof

Code cleanup:

  • Added Locale.ROOT to toUpperCase() calls
  • var throughout all main source files
  • Single get()+null check instead of containsKey()+get() in renderer
  • Added proper Node import in AlertNodeRenderer
  • LinkedHashMapHashMap in AlertsExtension

README:

  • Fixed outdated custom type documentation
  • Added screenshots (with and without icons)
  • Aligned examples with AlertsExample.java

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.

GFM admonition blocks (alerts)

2 participants