Mirror of Steamdown (Markdown -> Steam Markup converter)
0

Configure Feed

Select the types of activity you want to include in your feed.

Permit fenced code blocks with a language tag (#383)

The code block parser's hint and open regexes only matched a bare fence
followed immediately by a newline, so a fence with an info string such as
```js was never parsed as a code block. It fell through to inline parsing,
which dropped the raw fence markers and mangled braces in the code. Allow an
optional info string after the opening backticks (it is discarded, matching
the existing behavior of emitting a plain [code] block).

Co-authored-by: Spenser Black <me@spenser.black>

authored by

Aaron Scherer
Spenser Black
and committed by
GitHub
(Jul 15, 2026, 6:59 PM EDT) 5e09b5df acfb0ca4

+28 -2
+13
packages/steamdown/README.md
··· 221 221 link. Additionally, as you may have noticed from the `[img]` block, alt text is not 222 222 used. 223 223 224 + ### Code blocks 225 + 226 + Fenced code blocks render as `[code]` blocks: 227 + 228 + ````markdown 229 + ```csharp 230 + var x = 1; 231 + ``` 232 + ```` 233 + 234 + You can put a language after the opening fence, but Steam has no syntax highlighting, 235 + so the language identifier is ignored. 236 + 224 237 ### Inline code (`` `code` ``) 225 238 226 239 Steam seems to render all `[code]` tags as blocks, so inline code is not supported.
+13
packages/steamdown/__tests__/feedback.test.js
··· 12 12 [291, "[![Wikipedia](https://example.com/example.jpg)](https://example.com/example.jpg)", "[url=https://example.com/example.jpg][img]https://example.com/example.jpg[/img][/url]"], 13 13 [293, "test_underscore_test", "test_underscore_test"], 14 14 [293, "underscore_test_", "underscore_test_"], 15 + ["Fenced code block with a language tag", "```js\nconst x = 1;\n```", "[code]\nconst x = 1;\n[/code]"], 16 + ["Fenced code block without a language tag still works", "```\nconst x = 1;\n```", "[code]\nconst x = 1;\n[/code]"], 15 17 ]; 16 18 17 19 // ------------------------------------------------------------------------------------- ··· 27 29 const rendered = render(tree, context); 28 30 expect(rendered).toBe(expected); 29 31 }); 32 + 33 + test("an unterminated fence with a long info string does not blow up (ReDoS)", () => { 34 + // A run of backticks followed by non-newline chars and no closing newline is the 35 + // worst case for the fence regex. The info-string class must not overlap the 36 + // backtick run, or matching is quadratic. 200k chars finishes in ms when linear. 37 + const input = "`".repeat(200_000) + "x".repeat(200_000); 38 + const start = process.hrtime.bigint(); 39 + parse(input); 40 + const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6; 41 + expect(elapsedMs).toBeLessThan(1000); 42 + });
+2 -2
packages/steamdown/src/parser/block/code.ts
··· 10 10 * Parser for a code block node. 11 11 */ 12 12 export const code = { 13 - hint: (text: string) => /^```+\r?\n/.test(text), 13 + hint: (text: string) => /^`{3,}[^`\r\n]*\r?\n/.test(text), 14 14 parse: (text: string): [nodes.CodeBlock, remainder: string] => { 15 - const open = /^(```+)\r?\n/.exec(text); 15 + const open = /^(`{3,})[^`\r\n]*\r?\n/.exec(text); 16 16 if (!open) { 17 17 throw new UnreachableError(); 18 18 }