Skip to content

fix(backend) replace showdown (markdown) library#2711

Merged
grolu merged 10 commits intomasterfrom
enh/replace_showdownjs
Jan 22, 2026
Merged

fix(backend) replace showdown (markdown) library#2711
grolu merged 10 commits intomasterfrom
enh/replace_showdownjs

Conversation

@grolu
Copy link
Copy Markdown
Member

@grolu grolu commented Dec 23, 2025

Summary

This PR replaces the unmaintained showdown Markdown library with a modern, well-maintained Markdown processing pipeline based on unified / remark / rehype.

The goal was to:

  • Preserve GitHub-flavored Markdown behavior as closely as possible
  • Improve long-term maintainability and security
  • Avoid breaking our existing CommonJS-based test setup
  • Keep the production API changes minimal and explicit

Considerations: Choosing a Markdown Library

I evaluated several Markdown libraries with the requirement that GitHub-style Markdown should be supported either natively or via plugins.

Evaluated candidates

markdown-it

  • Core library is well maintained
  • Plugin ecosystem is partially unmaintained
  • Several important plugins are CommonJS-only
  • ESM interop is problematic in modern builds

marked

  • Actively maintained and fast

  • Limited plugin ecosystem

  • Missing features such as emoji support

  • Would require manual handling for:

    • External links
    • Task lists
    • GitHub-specific behavior

micromark

  • Excellent low-level parser
  • Very small and strict API
  • Plugin ecosystem is comparatively small
  • Requires a lot of custom glue code

unified / remark / rehype (chosen)

  • Actively maintained
  • Widely adopted in the ecosystem
  • Built on top of micromark
  • Large plugin ecosystem for GitHub-flavored Markdown
  • Clear separation between parsing, transformation, and output
  • Downside: larger dependency footprint

Given the trade-offs, unified/remark/rehype was chosen as the best long-term solution.


Handling the CommonJS Test Base

All modern Markdown libraries (except markdown-it) are ESM-only.
Our test suite, however, is still CommonJS-based.

To solve this cleanly:

  • The actual Markdown implementation was moved to
    markdown.engine.mjs (pure ESM)

  • Rollup is configured to copy this file to dist without transpiling

  • markdown.js acts as a wrapper:

    • Production code imports the engine directly (ESM)
    • The transpiled markdown.cjs dynamically loads the engine via await import()
  • The real markdown.engine.md is used by the markdown test suite only

  • All other indirect dependencies to the markdown engine have been replaced by a global mock

This allows:

  • Production code to use modern ESM
  • Tests to run against the real Markdown engine
  • Explicit, isolated async behavior

Additionally:

  • A snapshot test suite (markdown.spec.cjs) was introduced
  • This was done before the actual code changes to outline parsing differences
  • The original Showdown output was captured first
  • The new engine was iteratively aligned to match behavior where reasonable

Final Markdown Engine (markdown.engine.mjs)

The final implementation uses the following pipeline:

  • remark-parse – Markdown parsing
  • remark-gfm – GitHub-flavored Markdown
  • remark-breaks – GitHub-style line breaks
  • remark-emoji – Emoji support
  • remark-rehype – Markdown → HTML AST
  • rehype-slug – Heading IDs
  • rehype-external-links – External links (target="_blank")
  • rehype-stringify – HTML output
  • sanitize-html – Final security sanitization

Raw HTML is not parsed into the AST, but emitted as raw HTML and sanitized afterward.


Changes Compared to Showdown

Dropped features

  • Image dimension syntax

    ![alt](image.png =120x80)

    This is a Showdown-specific feature with no direct replacement.
    It is not standard GitHub Markdown and was removed intentionally.

Sanitization improvements

  • Explicit allow-list of GitHub-relevant tags:

    • details, summary
    • tables (table, thead, tbody, tr, th, td)
    • headings, code blocks, task lists, etc.
  • Explicitly allowed attributes (id, align, class, etc.)

  • GitHub task lists supported via disabled checkbox inputs

  • transformTags ensures:

    • All links opened in a new tab have noopener noreferrer
    • Only disabled checkbox inputs are allowed

Why still use sanitize-html (and not rehype-sanitize)?

While rehype-sanitize would integrate nicely with the AST and be slightly faster:

  • sanitize-html is the de-facto industry standard
  • It is widely audited and well understood
  • We already need to sanitize non-Markdown HTML elsewhere (e.g. branding)
  • Using a single sanitization library everywhere reduces risk and complexity

Additional Helper: breakAfterInlineHtmlBlock

This PR also introduces a small helper function:

breakAfterInlineHtmlBlock(input)

Why this is needed

When users paste HTML blocks directly into Markdown (for example <details>, <table>, or headings), Markdown parsers can sometimes “swallow” the following Markdown content if it appears immediately after a closing HTML tag without a blank line.

Example:

<details>
<summary>More</summary>
Some text
</details>
This should be a new paragraph

Without an explicit blank line, many Markdown parsers interpret the following content as part of the HTML block, leading to unexpected rendering.

What the function does

  • Scans the Markdown source text (before parsing)
  • Detects closing HTML tags that commonly start HTML blocks
  • Ensures there is at least one blank line (\n\n) after the closing tag
  • Only applies the fix when non-whitespace content follows
  • Leaves all other content untouched

Why not solve this in the parser?

This behavior is part of the Markdown specification’s HTML block rules, not a bug in remark or rehype.
Applying this normalization before parsing avoids complex AST manipulation and keeps the pipeline predictable.


Async API Changes

The new Markdown engine is asynchronous.

To handle this safely in production code:

  • All call sites were updated accordingly

  • In the config service, a lazy cache was introduced:

    • Sanitization runs on first request
    • Results are cached (including in-flight promises)
    • No top-level await is required

⚠️ Note: This cache does not currently handle live configuration updates.
This matches existing behavior and can be improved separately if needed.


Conclusion

This PR modernizes Markdown handling with a secure, extensible, and well-maintained solution while:

  • Preserving GitHub-style behavior
  • Keeping test compatibility
  • Minimizing production code changes
  • Improving long-term maintainability and security

Which issue(s) this PR fixes:
Fixes #2707

Special notes for your reviewer:

Release note:

@gardener-robot gardener-robot added needs/review Needs review size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. needs/second-opinion Needs second review by someone else labels Dec 23, 2025
@gardener-robot
Copy link
Copy Markdown

@klocke-io, @holgerkoser You have pull request review open invite, please check

Copy link
Copy Markdown
Member

@klocke-io klocke-io left a comment

Choose a reason for hiding this comment

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

/lgtm

@gardener-robot gardener-robot added reviewed/lgtm Has approval for merging and removed needs/review Needs review needs/second-opinion Needs second review by someone else labels Jan 5, 2026
Comment thread backend/lib/routes/config.js Outdated
Comment thread backend/lib/routes/config.js
Comment thread backend/lib/services/terminals/index.js Outdated
Comment thread backend/lib/services/tickets.js Outdated
Maybe we will introduce them later, keep this PR simple for now
@gardener-robot gardener-robot added needs/second-opinion Needs second review by someone else and removed reviewed/lgtm Has approval for merging labels Jan 8, 2026
Comment thread backend/lib/routes/config.js Outdated
Comment thread backend/lib/services/terminals/index.js Outdated
}
const githubIssues = await searchIssues({ state: 'open', title })
return _.map(githubIssues, fromIssue)
return Promise.all(_.map(githubIssues || [], fromIssue))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

did you encounter a case where adding the fallback || [] was necessary?

Copy link
Copy Markdown
Member Author

@grolu grolu Jan 9, 2026

Choose a reason for hiding this comment

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

Yes I think if the request failed it was undefined but not 100% sure

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

then in line 134, do we also need the fallback out of the same reasons or does it behave differently and therefore not needed?
return Promise.all(_.map(githubComments, githubComment =>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

hm probably this error did never occur because if issues failed with error it will never try to load comments, but yes if this fails we should also have a fallback. I will add it

Comment thread backend/lib/markdown.engine.mjs Outdated
Comment thread backend/lib/markdown.engine.mjs
@gardener-prow gardener-prow Bot added cla: yes Indicates the PR's author has signed the cla-assistant.io CLA. approved Indicates a PR has been approved by an approver from all required OWNERS files. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Jan 22, 2026
Copy link
Copy Markdown
Member

@petersutter petersutter left a comment

Choose a reason for hiding this comment

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

/approve

@gardener-prow
Copy link
Copy Markdown

gardener-prow Bot commented Jan 22, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: klocke-io, petersutter

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:
  • OWNERS [klocke-io,petersutter]

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@gardener-prow gardener-prow Bot added the lgtm Indicates that a PR is ready to be merged. label Jan 22, 2026
@gardener-prow
Copy link
Copy Markdown

gardener-prow Bot commented Jan 22, 2026

LGTM label has been added.

DetailsGit tree hash: 48acc871d2fbc72fbb12ca635401986b980239fa

@gardener-prow gardener-prow Bot removed the lgtm Indicates that a PR is ready to be merged. label Jan 22, 2026
@gardener-prow
Copy link
Copy Markdown

gardener-prow Bot commented Jan 22, 2026

New changes are detected. LGTM label has been removed.

@grolu grolu merged commit 3660a51 into master Jan 22, 2026
19 checks passed
@grolu grolu deleted the enh/replace_showdownjs branch January 22, 2026 11:38
@grolu
Copy link
Copy Markdown
Member Author

grolu commented Jan 22, 2026

/cherry-pick hotfix-1.83

@gardener-ci-robot
Copy link
Copy Markdown
Contributor

@grolu: new pull request created: #2728

Details

In response to this:

/cherry-pick hotfix-1.83

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

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

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. cla: yes Indicates the PR's author has signed the cla-assistant.io CLA. needs/second-opinion Needs second review by someone else size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace showdownjs/showdown with alternative

5 participants