#!/usr/bin/env node import { createHash } from "node:crypto" import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" import { readFile, rename, writeFile } from "node:fs/promises" import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path" import { fileURLToPath } from "node:url" import { parseArgs } from "node:util" import { cosmiconfig } from "cosmiconfig" import ignore, { type Ignore } from "ignore" import { glob } from "tinyglobby" import { MARKDOWN_ONLY_OPTION_KEYS, MARKDOWN_OPTION_KEYS, type MarkdownOptions, transformMarkdown } from "./markdown.js" import { HTML_ONLY_OPTION_KEYS, HTML_OPTION_KEYS, type HtmlOptions, transformHtml } from "./html.js" import { PUNCTUATION_STYLES, type PunctuationStyle } from "./quotes.js" import { DASH_STYLES, type DashStyle } from "./dashes.js" import { assertKnownOptionKeys, omitKeys, stableStringify } from "./utils.js" type CliOptions = MarkdownOptions & HtmlOptions type FileType = "md" | "html" interface CliIO { stdin: AsyncIterable stdout: NodeJS.WritableStream stderr: NodeJS.WritableStream } class UsageError extends Error {} const FILE_TYPES = ["md", "html"] as const satisfies readonly FileType[] const EMPHASIS_MARKERS = ["*", "_"] as const const BULLET_MARKERS = ["-", "*", "+"] as const const RULE_MARKERS = ["-", "*", "_"] as const const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown"]) const HTML_EXTENSIONS = new Set([".html", ".htm"]) // camelCased flag values; buildFlags validates choice literals at runtime. interface ParsedFlags { write?: boolean check?: boolean cache?: boolean cacheLocation?: string config?: string | false // string if --config , false if --no-config ignorePath?: string stdinFilepath?: string type?: FileType punctuationStyle?: PunctuationStyle dashStyle?: DashStyle symbols?: boolean nbsp?: boolean collapseSpaces?: boolean arrows?: boolean fractions?: boolean degrees?: boolean superscript?: boolean ligatures?: boolean emphasisMarker?: "*" | "_" strongMarker?: "*" | "_" bulletMarker?: "-" | "*" | "+" ruleMarker?: "-" | "*" | "_" skipTag?: string[] skipClass?: string[] fragment?: boolean } const PARSE_ARGS_OPTIONS = { "bullet-marker": { type: "string" }, "cache-location": { type: "string" }, check: { type: "boolean" }, config: { type: "string" }, "dash-style": { type: "string" }, degrees: { type: "boolean" }, "emphasis-marker": { type: "string" }, fractions: { type: "boolean" }, help: { short: "h", type: "boolean" }, "ignore-path": { type: "string" }, ligatures: { type: "boolean" }, nbsp: { type: "boolean" }, "no-arrows": { type: "boolean" }, "no-cache": { type: "boolean" }, "no-collapse-spaces": { type: "boolean" }, "no-config": { type: "boolean" }, "no-fragment": { type: "boolean" }, "no-nbsp": { type: "boolean" }, "no-symbols": { type: "boolean" }, "punctuation-style": { type: "string" }, "rule-marker": { type: "string" }, "skip-class": { multiple: true, type: "string" }, "skip-tag": { multiple: true, type: "string" }, "stdin-filepath": { type: "string" }, "strong-marker": { type: "string" }, superscript: { type: "boolean" }, type: { type: "string" }, version: { short: "V", type: "boolean" }, write: { type: "boolean" }, } as const const HELP_TEXT = `Usage: punctilio [options] [files...] Apply typographic improvements to Markdown and HTML files. Arguments: files files to format (pass '-' to read from stdin) Options: -V, --version show version --write rewrite files in place (default prints formatted output to stdout) --check exit 1 if any file would change; write nothing --config path to a punctilio config file (otherwise auto-discovered) --no-config ignore any auto-discovered config file --ignore-path path to an ignore file (otherwise .punctilioignore in cwd) --no-cache disable the incremental cache (default location node_modules/.cache/punctilio/cache.json) --cache-location override the cache file location --stdin-filepath treat stdin as if it were this filename (infers type from extension) --type force file type (otherwise inferred from extension) (choices: ${FILE_TYPES.join(", ")}) --punctuation-style