import { readFileSync, writeFileSync } from "fs"; import { unified } from "unified"; import remarkParse from "remark-parse"; import remarkGfm from "remark-gfm"; import remarkFrontmatter from "remark-frontmatter"; import remarkStringify from "remark-stringify"; import { visit } from "unist-util-visit"; import YAML from "yaml"; import { fixCjkEmphasisSpacing, CJK_CLOSING_PUNCT_RE, CJK_OPENING_PUNCT_RE, CJK_CHAR_RE, } from "./cjk-emphasis"; import { replaceQuotes } from "./quotes"; import { applyAutocorrect } from "./autocorrect"; export interface FormatOptions { quotes?: boolean; spacing?: boolean; emphasis?: boolean; } export interface FormatResult { success: boolean; filePath: string; quotesFixed: boolean; spacingApplied: boolean; emphasisFixed: boolean; error?: string; } const DEFAULT_OPTIONS: Required = { quotes: false, spacing: true, emphasis: true, }; function formatFrontmatter(value: string): string | null { try { const doc = YAML.parseDocument(value); return doc.toString({ lineWidth: 0 }).trimEnd(); } catch { return null; } } function formatMarkdownContent( content: string, options: Required ): string { if (options.emphasis) { content = fixCjkEmphasisSpacing(content); } const processor = unified() .use(remarkParse) .use(remarkGfm) .use(remarkFrontmatter, ["yaml"]) .use(remarkStringify, { wrap: false, }); const tree = processor.parse(content); visit(tree, (node, _index, parent) => { if (node.type === "text" && options.quotes) { const textNode = node as { value: string }; textNode.value = replaceQuotes(textNode.value); return; } if (node.type === "yaml") { const yamlNode = node as { value: string }; const formatted = formatFrontmatter(yamlNode.value); if (formatted !== null) { yamlNode.value = formatted; } return; } if ( options.emphasis && (node.type === "strong" || node.type === "emphasis" || node.type === "delete") && parent ) { const siblings = (parent as { children: typeof node[] }).children; const idx = siblings.indexOf(node); const children = (node as { children: typeof node[] }).children; if (!children || children.length === 0) return; const lastChild = children[children.length - 1]; if (lastChild.type === "text") { const lastText = (lastChild as { value: string }).value; if ( CJK_CLOSING_PUNCT_RE.test(lastText.slice(-1)) && idx + 1 < siblings.length ) { const nextSib = siblings[idx + 1]; if (nextSib.type === "text") { const nextText = (nextSib as { value: string }).value; if (CJK_CHAR_RE.test(nextText.charAt(0))) { (nextSib as { value: string }).value = " " + nextText; } } } } const firstChild = children[0]; if (firstChild.type === "text") { const firstText = (firstChild as { value: string }).value; if (CJK_OPENING_PUNCT_RE.test(firstText) && idx > 0) { const prevSib = siblings[idx - 1]; if (prevSib.type === "text") { const prevText = (prevSib as { value: string }).value; if (CJK_CHAR_RE.test(prevText.charAt(prevText.length - 1))) { (prevSib as { value: string }).value = prevText + " "; } } } } } }); let result = processor.stringify(tree); if (options.emphasis) { result = fixCjkEmphasisSpacing(result); } return result; } export function formatMarkdown( filePath: string, options?: FormatOptions ): FormatResult { const opts: Required = { ...DEFAULT_OPTIONS, ...options }; const result: FormatResult = { success: false, filePath, quotesFixed: false, spacingApplied: false, emphasisFixed: false, }; try { const content = readFileSync(filePath, "utf-8"); const formattedContent = formatMarkdownContent(content, opts); result.quotesFixed = opts.quotes; result.emphasisFixed = opts.emphasis; writeFileSync(filePath, formattedContent, "utf-8"); if (opts.spacing) { result.spacingApplied = applyAutocorrect(filePath); } result.success = true; console.log(`✓ Formatted: ${filePath}`); } catch (error) { result.error = error instanceof Error ? error.message : String(error); console.error(`✗ Format failed: ${result.error}`); } return result; } function parseArgs(args: string[]): { filePath: string; options: FormatOptions } { const options: FormatOptions = {}; let filePath = ""; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "--quotes" || arg === "-q") { options.quotes = true; } else if (arg === "--no-quotes") { options.quotes = false; } else if (arg === "--spacing" || arg === "-s") { options.spacing = true; } else if (arg === "--no-spacing") { options.spacing = false; } else if (arg === "--emphasis" || arg === "-e") { options.emphasis = true; } else if (arg === "--no-emphasis") { options.emphasis = false; } else if (arg === "--help" || arg === "-h") { console.log(`Usage: npx -y bun scripts/main.ts [options] Options: -q, --quotes Replace ASCII quotes with fullwidth quotes (default: false) --no-quotes Do not replace quotes -s, --spacing Add CJK/English spacing via autocorrect (default: true) --no-spacing Do not add CJK/English spacing -e, --emphasis Fix CJK emphasis punctuation issues (default: true) --no-emphasis Do not fix CJK emphasis issues -h, --help Show this help message`); process.exit(0); } else if (!arg.startsWith("-")) { filePath = arg; } } return { filePath, options }; } if (import.meta.url === `file://${process.argv[1]}`) { const { filePath, options } = parseArgs(process.argv.slice(2)); if (!filePath) { console.error("Usage: npx -y bun scripts/main.ts [options]"); console.error("Use --help for more information."); process.exit(1); } const result = formatMarkdown(filePath, options); if (!result.success) { process.exit(1); } }