/** * Block-level HTML comment stripping for context and rule files. * * Claude Code strips block-level HTML comments (``) * from CLAUDE.md files before injection, while preserving comments inside code * blocks. "Block-level" is read as whole-line-anchored: a comment counts only * when it starts a line (after optional indentation) and nothing but comments * and whitespace occupy the line(s) it spans; those lines are removed entirely, * a multi-line comment with every line it covers. A comment sharing a line with * real content is inline prose and stays verbatim, as does anything inside a * fenced code block (backtick or tilde). */ /** The fence a line opens or closes, if any. */ export function fenceMarker(lineStart: string): string | null { if (lineStart.startsWith('```')) return '`' if (lineStart.startsWith('~~~')) return '~' return null } /** Length of the fence run starting `lineStart`, a run of `marker` characters. */ function fenceLength(lineStart: string, marker: string): number { let length = 0 while (length < lineStart.length && lineStart[length] === marker) length++ return length } // CommonMark closes a fenced block only with a fence of the same character that // is at least as long as the opener, so both are tracked: a shorter same-char // fence line (the classic 3-backtick block quoted inside a 4-backtick one) is // content, not a closer. export interface Fence { marker: string length: number } /** The fence state after a line, plus whether the line is fenced code (opener, * body, or closer) and so emitted verbatim rather than scanned for comments. */ export function stepFence(fence: Fence | null, trimmed: string, marker: string | null): { fence: Fence | null; fenced: boolean } { if (marker !== null && fence === null) { return { fence: { marker, length: fenceLength(trimmed, marker) }, fenced: true } } if (fence !== null) { const closes = marker === fence.marker && fenceLength(trimmed, marker) >= fence.length return { fence: closes ? null : fence, fenced: true } // closer included: comments are content, not maintainer notes } return { fence, fenced: false } } /** Advance a line while inside an open multi-line comment. Emits any content that * trails the closer onto its own line; returns whether the comment stays open. */ function continueOpenComment(out: string[], line: string): boolean { const close = line.indexOf('-->') if (close === -1) return true // still inside the comment: the line goes with it const rest = line.slice(close + 3) // Content trailing the closer keeps its line; a bare closer line is dropped. if (rest.trim().length > 0) out.push(rest.trimStart()) return false } /** Fold line-starting HTML comments off `trimmed`; several may share one line. * `allComment` means the line held nothing but comments and whitespace, so it is * dropped; `opensBlock` means the last comment opened a multi-line block. */ function consumeLineComments(trimmed: string): { allComment: boolean; opensBlock: boolean } { let rest = trimmed let sawComment = false while (rest.startsWith('') if (close === -1) return { allComment: true, opensBlock: true } // opens a multi-line comment rest = rest.slice(close + 3).trimStart() } return { allComment: sawComment && rest.length === 0, opensBlock: false } } /** Remove whole-line HTML comments, keeping fenced code and inline comments. */ export function stripBlockComments(text: string): string { const out: string[] = [] let fence: Fence | null = null let inComment = false for (const line of text.split('\n')) { if (inComment) { inComment = continueOpenComment(out, line) continue } const trimmed = line.trimStart() const step = stepFence(fence, trimmed, fenceMarker(trimmed)) fence = step.fence if (step.fenced) { out.push(line) continue } const { allComment, opensBlock } = consumeLineComments(trimmed) if (opensBlock) inComment = true if (allComment) continue // the whole line was comment // No comment, or a line-starting comment followed by content: inline, verbatim. out.push(line) } return out.join('\n') }