diff --git a/skills/baoyu-format-markdown/SKILL.md b/skills/baoyu-format-markdown/SKILL.md index 90bf1cd..c7aa57b 100644 --- a/skills/baoyu-format-markdown/SKILL.md +++ b/skills/baoyu-format-markdown/SKILL.md @@ -58,6 +58,55 @@ Claude performs content analysis and formatting (Steps 1-6), then runs the scrip Read the user-specified markdown or plain text file. +### Step 1.5: Detect Content Type & Confirm + +**Content Type Detection:** + +| Indicator | Classification | +|-----------|----------------| +| Has `---` YAML frontmatter | Markdown | +| Has `#`, `##`, `###` headings | Markdown | +| Has `**bold**`, `*italic*` | Markdown | +| Has `- ` or `1. ` lists | Markdown | +| Has ``` code blocks | Markdown | +| Has `> ` blockquotes | Markdown | +| None of above | Plain text | + +**Decision Flow:** + +┌─────────────────┬────────────────────────────────────────────────┐ +│ Content Type │ Action │ +├─────────────────┼────────────────────────────────────────────────┤ +│ Plain text │ Proceed to Step 2 (format to markdown) │ +├─────────────────┼────────────────────────────────────────────────┤ +│ Markdown │ Use AskUserQuestion to confirm optimization │ +└─────────────────┴────────────────────────────────────────────────┘ + +**If Markdown detected, ask user:** + +``` +Detected existing markdown formatting. What would you like to do? + +1. Optimize formatting (Recommended) + - Add/improve frontmatter, headings, bold, lists + - Run typography script (spacing, emphasis fixes) + - Output: {filename}-formatted.md + +2. Keep original formatting + - Preserve existing markdown structure + - Run typography script (spacing, emphasis fixes) + - Output: {filename}-formatted.md + +3. Typography fixes only + - Run typography script on original file in-place + - No copy created, modifies original file directly +``` + +**Based on user choice:** +- **Optimize**: Continue to Step 2-8 (full workflow) +- **Keep original**: Skip Steps 2-5, copy file → Step 6-8 (run script on copy) +- **Typography only**: Skip Steps 2-6, run Step 7 on original file directly + ### Step 2: Analyze Content Structure Identify: @@ -138,6 +187,10 @@ Examples: - `final.md` → `final-formatted.md` - `draft-v1.md` → `draft-v1-formatted.md` +**If user chose "Keep original formatting" (from Step 1.5):** +- Copy original file to `{filename}-formatted.md` without modifications +- Proceed to Step 7 for typography fixes only + **Backup existing file:** If `{filename}-formatted.md` already exists, backup before overwriting: diff --git a/skills/baoyu-format-markdown/scripts/cjk-emphasis.ts b/skills/baoyu-format-markdown/scripts/cjk-emphasis.ts index f1d618a..ebf5ad0 100644 --- a/skills/baoyu-format-markdown/scripts/cjk-emphasis.ts +++ b/skills/baoyu-format-markdown/scripts/cjk-emphasis.ts @@ -11,6 +11,21 @@ export const CJK_CHAR_RE = new RegExp(`[${CJK_SCRIPTS}]`, "u"); const PUNCT_OR_SYMBOL_RE = /[\p{P}\p{S}]/u; const WORD_CHAR_RE = /[\p{L}\p{N}]/u; +const CJK_PUNCT_PAIRS: Record = { + "“": "”", + "‘": "’", + "(": ")", + "〔": "〕", + "〖": "〗", + "〘": "〙", + "〚": "〛", + "「": "」", + "『": "』", + "〈": "〉", + "《": "》", + "【": "】", +}; + function findInlineCodeRanges(text: string): Array<[number, number]> { const ranges: Array<[number, number]> = []; let i = 0; @@ -69,8 +84,120 @@ function isPunctuationOrSymbol(ch: string | undefined): boolean { return !!ch && PUNCT_OR_SYMBOL_RE.test(ch); } -function fixCjkEmphasisSpacingInBlock(block: string): string { +function isMatchingCjkPunct(open: string, close: string): boolean { + return CJK_PUNCT_PAIRS[open] === close; +} + +function mapNonCodeSegments( + block: string, + mapper: (segment: string) => string +): string { const codeRanges = findInlineCodeRanges(block); + if (codeRanges.length === 0) { + return mapper(block); + } + + let result = ""; + let lastIndex = 0; + for (const [start, end] of codeRanges) { + if (start > lastIndex) { + result += mapper(block.slice(lastIndex, start)); + } + result += block.slice(start, end + 1); + lastIndex = end + 1; + } + if (lastIndex < block.length) { + result += mapper(block.slice(lastIndex)); + } + return result; +} + +function moveCjkPunctuationOutsideEmphasis(block: string): string { + return mapNonCodeSegments(block, (segment) => { + const delimiterPositions: number[] = []; + let cursor = 0; + while (cursor < segment.length - 1) { + if ( + segment[cursor] === "*" && + segment[cursor + 1] === "*" && + segment[cursor - 1] !== "*" && + segment[cursor + 2] !== "*" && + !isEscaped(segment, cursor) + ) { + delimiterPositions.push(cursor); + cursor += 2; + continue; + } + cursor += 1; + } + + if (delimiterPositions.length < 2) return segment; + + const stack: number[] = []; + const pairs: Array<{ open: number; close: number }> = []; + for (const pos of delimiterPositions) { + if (stack.length === 0) { + stack.push(pos); + } else { + const open = stack.pop() as number; + pairs.push({ open, close: pos }); + } + } + + const skip = new Set(); + const insertBefore = new Map(); + + for (const pair of pairs) { + const openPunctPos = pair.open + 2; + const closePunctPos = pair.close - 1; + if (openPunctPos >= closePunctPos) continue; + + const openPunct = segment[openPunctPos]; + const closePunct = segment[closePunctPos]; + if (!openPunct || !closePunct) continue; + if (!CJK_OPENING_PUNCT_RE.test(openPunct)) continue; + if (!CJK_CLOSING_PUNCT_RE.test(closePunct)) continue; + if (!isMatchingCjkPunct(openPunct, closePunct)) continue; + if (openPunctPos + 1 >= closePunctPos) continue; + + const inner = segment.slice(openPunctPos + 1, closePunctPos); + if (inner.length === 0) continue; + + skip.add(openPunctPos); + skip.add(closePunctPos); + + insertBefore.set(pair.open, (insertBefore.get(pair.open) ?? "") + openPunct); + const afterClose = pair.close + 2; + insertBefore.set( + afterClose, + (insertBefore.get(afterClose) ?? "") + closePunct + ); + } + + if (skip.size === 0) return segment; + + let result = ""; + for (let idx = 0; idx < segment.length; idx += 1) { + const insert = insertBefore.get(idx); + if (insert) { + result += insert; + } + if (skip.has(idx)) { + continue; + } + result += segment[idx]; + } + const tailInsert = insertBefore.get(segment.length); + if (tailInsert) { + result += tailInsert; + } + return result; + }); +} + +function fixCjkEmphasisSpacingInBlock(block: string): string { + const normalized = moveCjkPunctuationOutsideEmphasis(block); + const codeRanges = findInlineCodeRanges(normalized); let rangeIndex = 0; const delimiters: Array<{ @@ -79,7 +206,7 @@ function fixCjkEmphasisSpacingInBlock(block: string): string { canClose: boolean; }> = []; let cursor = 0; - while (cursor < block.length - 1) { + while (cursor < normalized.length - 1) { if (rangeIndex < codeRanges.length && cursor >= codeRanges[rangeIndex][0]) { if (cursor <= codeRanges[rangeIndex][1]) { cursor = codeRanges[rangeIndex][1] + 1; @@ -90,14 +217,14 @@ function fixCjkEmphasisSpacingInBlock(block: string): string { } if ( - block[cursor] === "*" && - block[cursor + 1] === "*" && - block[cursor - 1] !== "*" && - block[cursor + 2] !== "*" && - !isEscaped(block, cursor) + normalized[cursor] === "*" && + normalized[cursor + 1] === "*" && + normalized[cursor - 1] !== "*" && + normalized[cursor + 2] !== "*" && + !isEscaped(normalized, cursor) ) { - const before = block[cursor - 1]; - const after = block[cursor + 2]; + const before = normalized[cursor - 1]; + const after = normalized[cursor + 2]; const beforeIsSpace = isWhitespaceChar(before); const afterIsSpace = isWhitespaceChar(after); const beforeIsPunct = isPunctuationOrSymbol(before); @@ -140,12 +267,12 @@ function fixCjkEmphasisSpacingInBlock(block: string): string { } } - if (pairs.length === 0) return block; + if (pairs.length === 0) return normalized; const insertPositions = new Set(); for (const pair of pairs) { - const insideLast = block[pair.close - 1]; - const afterClose = block[pair.close + 2]; + const insideLast = normalized[pair.close - 1]; + const afterClose = normalized[pair.close + 2]; if (!afterClose) continue; if ( CJK_CLOSING_PUNCT_RE.test(insideLast) && @@ -155,16 +282,16 @@ function fixCjkEmphasisSpacingInBlock(block: string): string { } } - if (insertPositions.size === 0) return block; + if (insertPositions.size === 0) return normalized; let result = ""; - for (let idx = 0; idx < block.length; idx += 1) { + for (let idx = 0; idx < normalized.length; idx += 1) { if (insertPositions.has(idx)) { result += " "; } - result += block[idx]; + result += normalized[idx]; } - if (insertPositions.has(block.length)) { + if (insertPositions.has(normalized.length)) { result += " "; } return result;