Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-markdown-heading-newline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vortexlabs/kirari': patch
---

Fix Markdown heading tokens including the trailing line break. The heading now ends at the end of its line and the line break is a separate text token, so line-based renderers and heading styles stop at the line end.
10 changes: 8 additions & 2 deletions packages/kirari/src/languages/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ const markdownTokens = (source: string, language: Language): Token[] => {
const emit = (type: string, start: number, end: number) => {
if (end > start) tokens.push({ type, value: source.slice(start, end), start, end })
}
// A heading ends at its line break; the break itself is text, as after any other line.
const emitHeading = (start: number, line: string) => {
const end = start + line.replace(/[\r\n]+$/, '').length
emit('heading', start, end)
emit('text', end, start + line.length)
}
const emitInline = (value: string, start: number) => {
for (const token of tokenize(value, base)) tokens.push({ ...token, start: token.start + start, end: token.end + start })
}
Expand All @@ -38,11 +44,11 @@ const markdownTokens = (source: string, language: Language): Token[] => {
fence = { char: open[1]![0]!, length: open[1]!.length, start: offset }
} else if (/^ {0,3}#{1,6}(?:[ \t]|$)/.test(body)
|| (body.trim() && /^ {0,3}(?:=+|-+)[ \t]*(?:\r?\n|\r)?$/.test(lines[index + 1] ?? ''))) {
emit('heading', offset, offset + line.length)
emitHeading(offset, line)
if (!/^ {0,3}#/.test(body)) {
offset += line.length
const underline = lines[++index]!
emit('heading', offset, offset + underline.length)
emitHeading(offset, underline)
offset += underline.length
continue
}
Expand Down
6 changes: 4 additions & 2 deletions packages/kirari/test/common-formats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ test('CSS selectors, custom properties, dimensions, colors, and functions', () =
test('Markdown headings, links, inline code, and fence length', () => {
const source = '# Heading\r\nText **bold** and [site](https://example.com), ``a ` b``.\n~~~~ js\n# code\n~~~\n~~~~\nAfter\n'
const result = pairs(source, markdown)
expect(result).toContainEqual(['heading', '# Heading\r\n'])
expect(result).toContainEqual(['heading', '# Heading'])
expect(result).toContainEqual(['string', '[site](https://example.com)'])
expect(result).toContainEqual(['string', '``a ` b``'])
expect(result).toContainEqual(['string', '~~~~ js\n# code\n~~~\n~~~~\n'])
expect(pairs('Title\n=====\n', markdown)).toEqual([['heading', 'Title\n'], ['heading', '=====\n']])
expect(pairs('Title\n=====\n', markdown)).toEqual([['heading', 'Title'], ['heading', '=====']])
expect(tokenize('# Title\nnext', markdown).map(token => [token.type, token.value])).toEqual([['heading', '# Title'], ['text', '\n'], ['text', 'next']])
expect(tokenize('Title\r\n---\r\n', markdown).map(token => [token.type, token.value])).toEqual([['heading', 'Title'], ['text', '\r\n'], ['heading', '---'], ['text', '\r\n']])
expect(pairs('```js\nunfinished', markdown)).toEqual([['string', '```js\nunfinished']])
})

Expand Down
6 changes: 3 additions & 3 deletions packages/kirari/test/markdown-fences.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const pairs = (source: string, language = enhanced) => tokenize(source, language
test('highlights multilingual fences using the first info word and keeps Markdown outside', () => {
const source = '# Examples\n```js title="demo"\nconst answer = 42\n```\n~~~PY\ndef greet():\n return True\n~~~\n```data.json\n{"ready": true}\n```\nText **bold** and `code`.\n'
const result = pairs(source)
for (const pair of [['heading', '# Examples\n'], ['string', '```js title="demo"\n'], ['keyword', 'const'],
for (const pair of [['heading', '# Examples'], ['text', '\n'], ['string', '```js title="demo"\n'], ['keyword', 'const'],
['keyword', 'def'], ['boolean', 'True'], ['property', '"ready"'], ['boolean', 'true'], ['string', '`code`']]) {
expect(result).toContainEqual(pair)
}
Expand All @@ -35,7 +35,7 @@ test.each([
'```js\nconst answer = 42\n``` trailing\n```\n# After\n',
])('reuses Markdown fence indentation, character, length, and trailing-text boundaries', source => {
expect(pairs(source)).toContainEqual(['keyword', 'const'])
expect(pairs(source)).toContainEqual(['heading', source.includes('\r\n') ? '# After\r\n' : '# After\n'])
expect(pairs(source).slice(-2)).toEqual([['heading', '# After'], ['text', source.includes('\r\n') ? '\r\n' : '\n']])
expect(tokenize(source, enhanced).map(token => token.value).join('')).toBe(source)
})

Expand Down Expand Up @@ -78,7 +78,7 @@ test('renders escaped source and retains caller-provided outer Markdown rules',
test('does not recursively inject languages into Markdown fences', () => {
const nested = createMarkdown(createRegistry([enhanced, javascript]))
const source = '````md\n# Inside\n```js\nconst x = 1\n```\n````\n'
expect(pairs(source, nested)).toContainEqual(['heading', '# Inside\n'])
expect(pairs(source, nested)).toContainEqual(['heading', '# Inside'])
expect(pairs(source, nested)).toContainEqual(['string', '```js\nconst x = 1\n```\n'])
expect(pairs(source, nested)).not.toContainEqual(['keyword', 'const'])
})
9 changes: 9 additions & 0 deletions sites/docs/changelog/kirari/1.1.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
package: kirari
version: 1.1.3
date: 2026-10-22
---

### Bug fixes

- Markdown heading tokens no longer include the trailing line break, so heading styles and line-based renderers stop at the end of the line.
Loading