chore(deps): update marked to 18.1.1 (#2382)

This commit is contained in:
JATMN
2026-10-24 02:52:27 -07:00
committed by GitHub
parent 95e96e9c16
commit d62eb70da1
10 changed files with 272 additions and 14 deletions
+23
View File
@@ -180,6 +180,29 @@ Before submitting, run multiple rounds of review on generated code:
Self-review up front saves everyone time and reduces back-and-forth during maintainer review.
### Markdown parsing and rendering
Use the exact `marked` version in `package.json` and its bundled TypeScript types;
import `marked`, `Lexer`, `Token`, or `Tokens` from the package's ESM entry point.
Do not add `@types/marked` or a separate rendering extension. Use
`configureMarked()` before terminal lexing and `formatToken()` / `applyMarkdown()`
for terminal output; strikethrough parsing is deliberately disabled.
List items can contain `paragraph` children and `checkbox` tokens. Preserve list
markers for paragraph children and omit checkbox glyphs in terminal output. Pass
the previous sibling to `formatToken()` when rendering block sequences so separate
`space` tokens do not duplicate spacing after headings, tables, or indented code.
Indented code has trailing newlines removed for terminal display and `/copy`;
fenced code retains its content. Keep streaming boundary calculations on the
complete lexer token stream, including whitespace and each token's `raw` length.
CLAUDE.md parsing uses its own `new Lexer({ gfm: false })` to preserve include
paths. When removing a comment-only HTML block, consume its following whitespace
token too; preserve whitespace with visible comment residue and leave code blocks
untouched. When updating `marked`, compare representative terminal, table, list,
code, link, streaming, and memory-file output and run the markdown regression tests.
Regenerate the exact dependency pin and lockfile with `bun add --exact marked@<version>`.
## AI Agent Guidelines
If you are an AI agent (Copilot, Cursor, Claude, etc.) working on this codebase, refer to [AGENTS.md](AGENTS.md) for project-specific coding guidelines, conventions, and validation commands. Following these guidelines will help your contributions align with the project's patterns and reduce review friction.
+2 -2
View File
@@ -62,7 +62,7 @@
"knip": "6.42.1",
"lodash-es": "4.18.1",
"lru-cache": "11.5.3",
"marked": "15.0.12",
"marked": "18.1.1",
"p-map": "7.1.1",
"picomatch": "4.0.7",
"proper-lockfile": "4.1.2",
@@ -989,7 +989,7 @@
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
"marked": ["marked@18.1.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+1 -1
View File
@@ -142,7 +142,7 @@
"knip": "6.42.1",
"lodash-es": "4.18.1",
"lru-cache": "11.5.3",
"marked": "15.0.12",
"marked": "18.1.1",
"p-map": "7.1.1",
"picomatch": "4.0.7",
"proper-lockfile": "4.1.2",
+3 -1
View File
@@ -34,7 +34,9 @@ function extractCodeBlocks(markdown: string): CodeBlock[] {
if (token.type === 'code') {
const codeToken = token as Tokens.Code;
blocks.push({
code: codeToken.text,
code: codeToken.codeBlockStyle === 'indented'
? codeToken.text.replace(/\n+$/, '')
: codeToken.text,
lang: codeToken.lang
});
}
+2 -2
View File
@@ -140,12 +140,12 @@ function MarkdownBody(t0) {
nonTableContent = "";
}
};
for (const token of tokens) {
for (const [index, token] of tokens.entries()) {
if (token.type === "table") {
flushNonTableContent();
elements.push(<MarkdownTable key={elements.length} token={token as Tokens.Table} highlight={highlight} />);
} else {
nonTableContent = nonTableContent + formatToken(token, theme, 0, null, null, highlight);
nonTableContent = nonTableContent + formatToken(token, theme, 0, null, null, highlight, tokens[index - 1]);
nonTableContent;
}
}
+88
View File
@@ -0,0 +1,88 @@
{
"inline": {
"input": "Plain **bold**, *italic*, `code`, ~~approximate~~ and \\*literal\\*.",
"terminal": "Plain bold, italic, code, ~~approximate~~ and *literal*.",
"codes": []
},
"headings": {
"input": "# Heading\n\nBody\n\n## Subheading\n\nNext",
"terminal": "Heading\n\nBody\n\nSubheading\n\nNext",
"codes": []
},
"blocks": {
"input": "> A quote\n> with **bold**\n\n---\n\nAfter",
"terminal": "▎ A quote\n▎ with bold\n\n---\nAfter",
"codes": []
},
"code": {
"input": "Before\n\n```ts\nconst n = 1\nconsole.log(n)\n```\n\nAfter\n\n indented()\n",
"terminal": "Before\n\nconst n = 1\nconsole.log(n)\n\nAfter\n\nindented()",
"codes": [
{
"code": "const n = 1\nconsole.log(n)",
"lang": "ts"
},
{
"code": "indented()"
}
]
},
"links": {
"input": "[Guide](https://example.com/guide) and <dev@example.com> and ![alt](image.png)",
"terminal": "https://example.com/guide and dev@example.com and image.png",
"codes": []
},
"tight": {
"input": "- First **bold**\n- Second\n continued\n 1. Nested one\n 2. Nested two\n- Last",
"terminal": "- First bold\n- Second\ncontinued\n a. Nested one\n b. Nested two\n- Last",
"codes": []
},
"loose": {
"input": "3. First **bold**\n\n4. Second\n\n Another paragraph\n\n5. Last",
"terminal": "3. First bold\n4. Second\n\n4. Another paragraph\n5. Last",
"codes": []
},
"tasks": {
"input": "- [ ] Pending\n- [x] Done\n\n- [ ] Loose task\n\n More text",
"terminal": "- Pending\n- Done\n- Loose task\n\n- More text",
"codes": []
},
"table": {
"input": "| Name | Value |\n| :--- | ---: |\n| **one** | `two` |\n| 三 | 42 |\n\nAfter",
"terminal": "| Name | Value |\n|------|-------|\n| one | two |\n| 三 | 42 |\n\nAfter",
"codes": []
},
"spacing": {
"input": "# Title\n\n\nBody\n\n```\ncode\n```\n\n\nAfter\n",
"terminal": "Title\n\nBody\n\ncode\n\nAfter",
"codes": [
{
"code": "code",
"lang": ""
}
]
},
"listBlocks": {
"input": "- Intro\n\n ```\n code\n ```\n\n > quote\n\n- End",
"terminal": "- Intro\n\ncode\n\n▎ quote\n- End",
"codes": []
},
"tableComponent": "┌──────┬───────┐\n│ Name │ Value │\n├──────┼───────┤\n│ one │ two │\n├──────┼───────┤\n│ 三 │ 42 │\n└──────┴───────┘",
"Markdown": "Heading\n\nBody\n\nSubheading\n\nNext\n\n- First bold\n- Second\ncontinued\n a. Nested one\n b. Nested two\n- Last\n\n┌──────┬───────┐\n│ Name │ Value │\n├──────┼───────┤\n│ one │ two │\n├──────┼───────┤\n│ 三 │ 42 │\n└──────┴───────┘\n\nAfter",
"StreamingMarkdown": "Heading\n\nBody\n\nSubheading\n\nNext\n\n- First bold\n- Second\ncontinued\n a. Nested one\n b. Nested two\n- Last\n\n┌──────┬───────┐\n│ Name │ Value │\n├──────┼───────┤\n│ one │ two │\n├──────┼───────┤\n│ 三 │ 42 │\n└──────┴───────┘\n\nAfter",
"memory": {
"input": "# Rules\n\n<!-- hidden -->\n\nUse **Bun**.\n\n```md\n<!-- keep -->\n@./ignored.md\n```\n\n@./included.md\n",
"content": "# Rules\n\nUse **Bun**.\n\n```md\n<!-- keep -->\n@./ignored.md\n```\n\n@./included.md\n",
"stripped": true
},
"memoryFiles": [
{
"name": "CLAUDE.md",
"content": "# Rules\n\nUse **Bun**.\n\n```md\n<!-- keep -->\n@./ignored.md\n```\n\n@./included.md\n"
},
{
"name": "included.md",
"content": "Included rule\n"
}
]
}
+68
View File
@@ -0,0 +1,68 @@
import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { stripVTControlCharacters as stripAnsi } from 'node:util'
import { marked, type Tokens } from 'marked'
import React from 'react'
import { call as copy } from '../../commands/copy/copy.js'
import { Markdown, StreamingMarkdown } from '../../components/Markdown.js'
import { MarkdownTable } from '../../components/MarkdownTable.js'
import { AppStateProvider, getDefaultAppState } from '../../state/AppState.js'
import { processMemoryFile, stripHtmlComments } from '../../utils/claudemd.js'
import { enableConfigs } from '../../utils/config.js'
import { applyMarkdown } from '../../utils/markdown.js'
import { renderToString } from '../../utils/staticRender.js'
import baseline from './markdownRendering.baseline.json'
// Run in a fresh process: exercise real rendering/configuration without shared
// module mocks or settings mutations from the rest of the unit suite.
enableConfigs()
const output: Record<string, unknown> = {}
for (const [name, fixture] of Object.entries(baseline)) {
if (typeof fixture !== 'object' || !('terminal' in fixture)) continue
let codes: { code: string; lang?: string }[] = []
if (fixture.codes.length) {
const picker = await copy(
() => { throw new Error('Expected the code block picker') },
{
messages: [{
type: 'assistant',
message: { content: [{ type: 'text', text: fixture.input }] },
}],
} as unknown as Parameters<typeof copy>[1],
'',
)
if (!React.isValidElement<{ codeBlocks: typeof codes }>(picker)) {
throw new Error('Missing code block picker')
}
codes = picker.props.codeBlocks
}
output[name] = {
input: fixture.input,
terminal: stripAnsi(applyMarkdown(fixture.input, 'dark')),
codes,
}
}
const table = marked.lexer(baseline.table.input).find(t => t.type === 'table') as Tokens.Table
output.tableComponent = await renderToString(
<MarkdownTable token={table} highlight={null} forceWidth={80} />, 80,
)
const defaultState = getDefaultAppState()
const state = {
...defaultState,
settings: { ...defaultState.settings, syntaxHighlightingDisabled: true },
}
const combined = [baseline.headings.input, baseline.tight.input, baseline.table.input].join('\n\n')
for (const Component of [Markdown, StreamingMarkdown]) {
output[Component.name] = await renderToString(
<AppStateProvider initialState={state}><Component>{combined}</Component></AppStateProvider>, 80,
)
}
output.memory = { input: baseline.memory.input, ...stripHtmlComments(baseline.memory.input) }
const dir = process.argv[2]!
await mkdir(dir, { recursive: true })
await writeFile(join(dir, 'CLAUDE.md'), baseline.memory.input)
await writeFile(join(dir, 'included.md'), 'Included rule\n')
output.memoryFiles = (
await processMemoryFile(join(dir, 'CLAUDE.md'), 'Project', new Set(), true)
).map(f => ({ name: f.path.split(/[\\/]/).at(-1), content: f.content }))
console.log(JSON.stringify(output))
+8 -2
View File
@@ -312,14 +312,20 @@ function stripHtmlCommentsFromTokens(tokens: ReturnType<Lexer['lex']>): {
let result = ''
let stripped = false
for (const token of tokens) {
for (let index = 0; index < tokens.length; index++) {
const token = tokens[index]!
if (token.type === 'html') {
const trimmed = token.raw.trimStart()
if (trimmed.startsWith('<!--') && trimmed.includes('-->')) {
// Per CommonMark, a type-2 HTML block ends at the *line* containing
// `-->`, so text after `-->` on that line is part of this token.
// Strip only the comment spans and keep any residual content.
const residue = stripHtmlCommentSpans(token.raw)
// marked emits trailing blank lines as a separate token. Remove them
// with a comment-only block, or retain them with its visible residue.
const next = tokens[index + 1]
const trailingSpace = next?.type === 'space' ? next.raw : ''
if (trailingSpace) index++
const residue = stripHtmlCommentSpans(token.raw + trailingSpace)
stripped = true
if (residue.trim().length > 0) {
// Residual content exists (e.g. `<!-- note --> Use bun`): keep it.
+45
View File
@@ -0,0 +1,45 @@
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import baseline from '../test/fixtures/markdownRendering.baseline.json'
let directory: string
let actual: Record<string, unknown>
beforeAll(() => {
directory = mkdtempSync(join(tmpdir(), 'openclaude-markdown-'))
const child = Bun.spawnSync([
process.execPath, 'run',
join(import.meta.dir, '../test/fixtures/markdownRendering.fixture.tsx'),
directory,
], {
env: {
...process.env,
CLAUDE_CONFIG_DIR: join(directory, 'config'),
OPENCLAUDE_CONFIG_DIR: join(directory, 'config'),
},
stdout: 'pipe',
stderr: 'pipe',
timeout: 15_000,
})
expect(child.exitCode, child.stderr.toString()).toBe(0)
expect(child.stderr.toString()).not.toContain('[Ink:uncaught]')
actual = JSON.parse(child.stdout.toString())
// The child has a 15-second deadline; allow another five seconds for setup
// and assertions when the test suite is sharing a busy runner.
}, 20_000)
afterAll(() => {
if (directory) rmSync(directory, { recursive: true, force: true })
})
// Captured through the same production paths using marked 15.0.12 before the
// dependency change. Assert rendered output, not the lexer's internal shape.
describe('markdown output compatibility', () => {
for (const [name, expected] of Object.entries(baseline)) {
test(name, () => {
expect(actual[name]).toEqual(expected)
})
}
})
+32 -6
View File
@@ -41,7 +41,9 @@ export function applyMarkdown(
configureMarked()
return marked
.lexer(stripPromptXMLTags(content))
.map(_ => formatToken(_, theme, 0, null, null, highlight))
.map((token, index, tokens) =>
formatToken(token, theme, 0, null, null, highlight, tokens[index - 1]),
)
.join('')
.trim()
}
@@ -53,11 +55,14 @@ export function formatToken(
orderedListNumber: number | null = null,
parent: Token | null = null,
highlight: CliHighlight | null = null,
previousToken?: Token,
): string {
switch (token.type) {
case 'blockquote': {
const inner = (token.tokens ?? [])
.map(_ => formatToken(_, theme, 0, null, null, highlight))
.map((child, index, tokens) =>
formatToken(child, theme, 0, null, null, highlight, tokens[index - 1]),
)
.join('')
// Prefix each line with a dim vertical bar. Keep text italic but at
// normal brightness — chalk.dim is nearly invisible on dark themes.
@@ -70,8 +75,12 @@ export function formatToken(
.join(EOL)
}
case 'code': {
const text =
token.codeBlockStyle === 'indented'
? token.text.replace(/\n+$/, '')
: token.text
if (!highlight) {
return token.text + EOL
return text + EOL
}
let language = 'plaintext'
if (token.lang) {
@@ -83,7 +92,7 @@ export function formatToken(
)
}
}
return highlight.highlight(token.text, { language }) + EOL
return highlight.highlight(text, { language }) + EOL
}
case 'codespan': {
// inline code
@@ -176,17 +185,34 @@ export function formatToken(
case 'list_item':
return (token.tokens ?? [])
.map(
_ =>
`${' '.repeat(listDepth)}${formatToken(_, theme, listDepth + 1, orderedListNumber, token, highlight)}`,
(child, index, tokens) =>
child.type === 'checkbox'
? ''
: `${' '.repeat(listDepth)}${formatToken(child, theme, listDepth + 1, orderedListNumber, token, highlight, tokens[index - 1])}`,
)
.join('')
case 'paragraph':
// Loose list children are paragraphs; retain the list marker path.
if (parent?.type === 'list_item') {
return formatToken(
{ ...token, type: 'text' }, theme, listDepth,
orderedListNumber, parent, highlight,
)
}
return (
(token.tokens ?? [])
.map(_ => formatToken(_, theme, 0, null, null, highlight))
.join('') + EOL
)
case 'space':
// These blocks already supply their spacing. Their trailing blank lines
// are separate space tokens in marked 18.
if (
previousToken && (
['heading', 'table', 'html', 'def'].includes(previousToken.type) ||
(previousToken.type === 'code' && previousToken.codeBlockStyle === 'indented')
)
) return ''
return EOL
case 'br':
return EOL