punctilio 3.13.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.pre-commit-hooks.yaml +1 -1
  2. package/README.md +30 -11
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +190 -60
  5. package/dist/cli.js.map +1 -1
  6. package/dist/dashes.js +4 -4
  7. package/dist/dashes.js.map +1 -1
  8. package/dist/html.d.ts +5 -0
  9. package/dist/html.d.ts.map +1 -1
  10. package/dist/html.js +8 -2
  11. package/dist/html.js.map +1 -1
  12. package/dist/index.d.ts +7 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +9 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/markdown.d.ts +9 -0
  17. package/dist/markdown.d.ts.map +1 -1
  18. package/dist/markdown.js +7 -1
  19. package/dist/markdown.js.map +1 -1
  20. package/dist/prettier-plugin.d.ts.map +1 -1
  21. package/dist/prettier-plugin.js +13 -1
  22. package/dist/prettier-plugin.js.map +1 -1
  23. package/dist/rehype.d.ts +15 -1
  24. package/dist/rehype.d.ts.map +1 -1
  25. package/dist/rehype.js +41 -12
  26. package/dist/rehype.js.map +1 -1
  27. package/dist/remark.d.ts +5 -0
  28. package/dist/remark.d.ts.map +1 -1
  29. package/dist/remark.js +7 -6
  30. package/dist/remark.js.map +1 -1
  31. package/dist/symbols.d.ts.map +1 -1
  32. package/dist/symbols.js +2 -3
  33. package/dist/symbols.js.map +1 -1
  34. package/dist/utils.d.ts +18 -1
  35. package/dist/utils.d.ts.map +1 -1
  36. package/dist/utils.js +40 -3
  37. package/dist/utils.js.map +1 -1
  38. package/package.json +2 -3
  39. package/src/cli.ts +200 -59
  40. package/src/dashes.ts +4 -4
  41. package/src/html.ts +11 -2
  42. package/src/index.ts +15 -4
  43. package/src/markdown.ts +15 -1
  44. package/src/prettier-plugin.ts +13 -1
  45. package/src/rehype.ts +60 -11
  46. package/src/remark.ts +13 -6
  47. package/src/symbols.ts +2 -3
  48. package/src/utils.ts +48 -3
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Formats a string for error messages, truncating above the threshold.
3
- * Writes full content to stderr in Node.js so build logs capture it.
3
+ * When the PUNCTILIO_DEBUG environment variable is set to a non-empty
4
+ * value, also writes the full content to stderr in Node.js so build logs
5
+ * capture it.
4
6
  */
5
7
  export declare function formatErrorString(content: string, label: string): string;
6
8
  /** Throws if any text node contains the separator (would corrupt split/join). */
@@ -18,11 +20,26 @@ interface TextNode {
18
20
  export declare function transformTextNodes(textNodes: TextNode[], transformFn: (input: string) => string, separator: string): void;
19
21
  /** Throws if a transformation changed the separator count. */
20
22
  export declare function assertSeparatorCountPreserved(original: string, transformed: string, separator?: string, transformName?: string): void;
23
+ /** Throws if `options` contains a key not present in `validKeys`. @internal */
24
+ export declare function assertKnownOptionKeys(options: object, validKeys: readonly string[], context: string): void;
25
+ /** Returns a copy of `obj` without the listed keys. @internal */
26
+ export declare function omitKeys<T extends object>(obj: T, keys: readonly string[]): Partial<T>;
21
27
  /** Returns a copy of `obj` with `undefined`-valued keys removed. @internal */
22
28
  export declare function filterUndefined<T extends object>(obj: T): Partial<T>;
23
29
  /** Deterministic JSON string for an options object, suitable as cache key. @internal */
24
30
  export declare function stableStringify(obj: object): string;
25
31
  /** Extracts named groups from a `.replace()` callback's arguments. @internal */
26
32
  export declare function namedGroups<G>(args: unknown[]): G;
33
+ /**
34
+ * Extracts the match offset and input string from a `.replace()` callback's
35
+ * arguments, independent of capture-group count. The trailing arguments are
36
+ * `..., offset, input[, namedGroups]`: the input is the last string (the
37
+ * named-groups object, when present, follows it) and the offset is the
38
+ * number immediately before the input. @internal
39
+ */
40
+ export declare function replaceCallbackContext(args: unknown[]): {
41
+ offset: number;
42
+ input: string;
43
+ };
27
44
  export {};
28
45
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAkBxE;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAanF;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,MAA0B,GAAG,MAAM,CAS3F;AAED,UAAU,QAAQ;IAChB,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,QAAQ,EAAE,EACrB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EACtC,SAAS,EAAE,MAAM,GAChB,IAAI,CAmBN;AAED,8DAA8D;AAC9D,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,SAAS,GAAE,MAA0B,EACrC,aAAa,GAAE,MAAoB,GAClC,IAAI,CAUN;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAMpE;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMnD;AAED,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,CAEjD"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAkBxE;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAanF;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,MAA0B,GAAG,MAAM,CAS3F;AAED,UAAU,QAAQ;IAChB,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,QAAQ,EAAE,EACrB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EACtC,SAAS,EAAE,MAAM,GAChB,IAAI,CAmBN;AAED,8DAA8D;AAC9D,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,SAAS,GAAE,MAA0B,EACrC,aAAa,GAAE,MAAoB,GAClC,IAAI,CAUN;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,OAAO,EAAE,MAAM,GACd,IAAI,CAQN;AAED,iEAAiE;AACjE,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAMtF;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAMpE;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMnD;AAED,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAUzF"}
package/dist/utils.js CHANGED
@@ -2,15 +2,17 @@ import { DEFAULT_SEPARATOR, ISSUES_URL } from "./constants.js";
2
2
  const ERROR_STRING_THRESHOLD = 2000;
3
3
  /**
4
4
  * Formats a string for error messages, truncating above the threshold.
5
- * Writes full content to stderr in Node.js so build logs capture it.
5
+ * When the PUNCTILIO_DEBUG environment variable is set to a non-empty
6
+ * value, also writes the full content to stderr in Node.js so build logs
7
+ * capture it.
6
8
  */
7
9
  export function formatErrorString(content, label) {
8
10
  if (content.length <= ERROR_STRING_THRESHOLD) {
9
11
  return JSON.stringify(content);
10
12
  }
11
- // In Node.js, write full content to stderr for debugging
13
+ // In Node.js, write full content to stderr for debugging when opted in
12
14
  try {
13
- if (typeof globalThis.process?.stderr?.write === "function") {
15
+ if (globalThis.process?.env?.PUNCTILIO_DEBUG && typeof globalThis.process?.stderr?.write === "function") {
14
16
  globalThis.process.stderr.write(`\n[punctilio ${label} full content (${content.length} chars)]:\n${content}\n\n`);
15
17
  }
16
18
  }
@@ -75,6 +77,23 @@ export function assertSeparatorCountPreserved(original, transformed, separator =
75
77
  `Include the input text that caused this error.`);
76
78
  }
77
79
  }
80
+ /** Throws if `options` contains a key not present in `validKeys`. @internal */
81
+ export function assertKnownOptionKeys(options, validKeys, context) {
82
+ for (const key of Object.keys(options)) {
83
+ if (!validKeys.includes(key)) {
84
+ throw new Error(`Unknown option "${key}" for ${context}. Valid options: ${[...validKeys].sort().join(", ")}.`);
85
+ }
86
+ }
87
+ }
88
+ /** Returns a copy of `obj` without the listed keys. @internal */
89
+ export function omitKeys(obj, keys) {
90
+ const result = {};
91
+ for (const [k, v] of Object.entries(obj)) {
92
+ if (!keys.includes(k))
93
+ result[k] = v;
94
+ }
95
+ return result;
96
+ }
78
97
  /** Returns a copy of `obj` with `undefined`-valued keys removed. @internal */
79
98
  export function filterUndefined(obj) {
80
99
  const result = {};
@@ -96,4 +115,22 @@ export function stableStringify(obj) {
96
115
  export function namedGroups(args) {
97
116
  return args[args.length - 1];
98
117
  }
118
+ /**
119
+ * Extracts the match offset and input string from a `.replace()` callback's
120
+ * arguments, independent of capture-group count. The trailing arguments are
121
+ * `..., offset, input[, namedGroups]`: the input is the last string (the
122
+ * named-groups object, when present, follows it) and the offset is the
123
+ * number immediately before the input. @internal
124
+ */
125
+ export function replaceCallbackContext(args) {
126
+ let inputIndex = args.length - 1;
127
+ while (inputIndex >= 0 && typeof args[inputIndex] !== "string") {
128
+ inputIndex--;
129
+ }
130
+ const offset = args[inputIndex - 1];
131
+ if (inputIndex < 1 || typeof offset !== "number") {
132
+ throw new Error("Could not locate offset and input in replace-callback arguments.");
133
+ }
134
+ return { offset, input: args[inputIndex] };
135
+ }
99
136
  //# sourceMappingURL=utils.js.map
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "punctilio",
3
- "version": "3.13.2",
3
+ "version": "4.1.0",
4
4
  "description": "Smart typography transformations: curly quotes, em-dashes, en-dashes, and more",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -76,7 +76,7 @@
76
76
  },
77
77
  "homepage": "https://github.com/alexander-turner/punctilio#readme",
78
78
  "engines": {
79
- "node": ">=18.0.0"
79
+ "node": ">=20.0.0"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@eslint/js": "9.39.4",
@@ -145,7 +145,6 @@
145
145
  }
146
146
  },
147
147
  "dependencies": {
148
- "commander": "14.0.3",
149
148
  "cosmiconfig": "9.0.1",
150
149
  "escape-string-regexp": "5.0.0",
151
150
  "ignore": "7.0.5",
package/src/cli.ts CHANGED
@@ -5,17 +5,17 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
5
5
  import { readFile, writeFile } from "node:fs/promises"
6
6
  import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path"
7
7
  import { fileURLToPath } from "node:url"
8
+ import { parseArgs } from "node:util"
8
9
 
9
- import { Command, CommanderError, Option } from "commander"
10
10
  import { cosmiconfig } from "cosmiconfig"
11
11
  import ignore, { type Ignore } from "ignore"
12
12
  import { glob } from "tinyglobby"
13
13
 
14
- import { type MarkdownOptions, transformMarkdown } from "./markdown.js"
15
- import { type HtmlOptions, transformHtml } from "./html.js"
14
+ import { MARKDOWN_ONLY_OPTION_KEYS, MARKDOWN_OPTION_KEYS, type MarkdownOptions, transformMarkdown } from "./markdown.js"
15
+ import { HTML_ONLY_OPTION_KEYS, HTML_OPTION_KEYS, type HtmlOptions, transformHtml } from "./html.js"
16
16
  import { PUNCTUATION_STYLES, type PunctuationStyle } from "./quotes.js"
17
17
  import { DASH_STYLES, type DashStyle } from "./dashes.js"
18
- import { stableStringify } from "./utils.js"
18
+ import { assertKnownOptionKeys, omitKeys, stableStringify } from "./utils.js"
19
19
 
20
20
  type CliOptions = MarkdownOptions & HtmlOptions
21
21
  type FileType = "md" | "html"
@@ -36,8 +36,9 @@ const RULE_MARKERS = ["-", "*", "_"] as const
36
36
  const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown"])
37
37
  const HTML_EXTENSIONS = new Set([".html", ".htm"])
38
38
 
39
- // Fields match commander's camelCased flag names; .choices() guarantees literals at runtime.
39
+ // camelCased flag values; buildFlags validates choice literals at runtime.
40
40
  interface ParsedFlags {
41
+ write?: boolean
41
42
  check?: boolean
42
43
  cache?: boolean
43
44
  cacheLocation?: string
@@ -64,42 +65,158 @@ interface ParsedFlags {
64
65
  fragment?: boolean
65
66
  }
66
67
 
67
- function buildProgram(version: string, io: CliIO): Command {
68
- return new Command()
69
- .name("punctilio")
70
- .description("Apply typographic improvements to Markdown and HTML files.")
71
- .version(version, "-V, --version", "show version")
72
- .argument("[files...]", "files to format (pass '-' to read from stdin)")
73
- .option("--check", "exit 1 if any file would change; write nothing")
74
- .option("--config <path>", "path to a punctilio config file (otherwise auto-discovered)")
75
- .option("--no-config", "ignore any auto-discovered config file")
76
- .option("--ignore-path <path>", "path to an ignore file (otherwise .punctilioignore in cwd)")
77
- .option("--no-cache", "disable the incremental cache (default location node_modules/.cache/punctilio/cache.json)")
78
- .option("--cache-location <path>", "override the cache file location")
79
- .option("--stdin-filepath <path>", "treat stdin as if it were this filename (infers type from extension)")
80
- .addOption(new Option("--type <type>", "force file type (otherwise inferred from extension)").choices(FILE_TYPES))
81
- .addOption(new Option("--punctuation-style <style>", "quote and punctuation style").choices(PUNCTUATION_STYLES))
82
- .addOption(new Option("--dash-style <style>", "dash style").choices(DASH_STYLES))
83
- .option("--no-symbols", "disable symbol transforms (ellipsis, ×, ©, etc.)")
84
- .option("--no-nbsp", "disable non-breaking space insertion")
85
- .option("--no-collapse-spaces", "preserve runs of consecutive spaces")
86
- .option("--no-arrows", "disable -> → arrow transforms")
87
- .option("--fractions", "enable 1/2 → ½")
88
- .option("--degrees", "enable 20 C → 20 °C")
89
- .option("--superscript", "enable 1st → 1ˢᵗ")
90
- .option("--ligatures", "enable !? → ⁉")
91
- .addOption(new Option("--emphasis-marker <char>", "markdown emphasis character").choices(EMPHASIS_MARKERS))
92
- .addOption(new Option("--strong-marker <char>", "markdown strong emphasis character").choices(EMPHASIS_MARKERS))
93
- .addOption(new Option("--bullet-marker <char>", "markdown list bullet character").choices(BULLET_MARKERS))
94
- .addOption(new Option("--rule-marker <char>", "markdown thematic break character").choices(RULE_MARKERS))
95
- .option("--skip-tag <tag...>", "HTML tags whose contents are left alone (repeatable)")
96
- .option("--skip-class <class...>", "HTML class names whose elements are skipped (repeatable)")
97
- .option("--no-fragment", "parse input as a full HTML document")
98
- .exitOverride()
99
- .configureOutput({
100
- writeOut: (str) => { io.stdout.write(str) },
101
- writeErr: (str) => { io.stderr.write(str) },
68
+ const PARSE_ARGS_OPTIONS = {
69
+ "bullet-marker": { type: "string" },
70
+ "cache-location": { type: "string" },
71
+ check: { type: "boolean" },
72
+ config: { type: "string" },
73
+ "dash-style": { type: "string" },
74
+ degrees: { type: "boolean" },
75
+ "emphasis-marker": { type: "string" },
76
+ fractions: { type: "boolean" },
77
+ help: { short: "h", type: "boolean" },
78
+ "ignore-path": { type: "string" },
79
+ ligatures: { type: "boolean" },
80
+ nbsp: { type: "boolean" },
81
+ "no-arrows": { type: "boolean" },
82
+ "no-cache": { type: "boolean" },
83
+ "no-collapse-spaces": { type: "boolean" },
84
+ "no-config": { type: "boolean" },
85
+ "no-fragment": { type: "boolean" },
86
+ "no-nbsp": { type: "boolean" },
87
+ "no-symbols": { type: "boolean" },
88
+ "punctuation-style": { type: "string" },
89
+ "rule-marker": { type: "string" },
90
+ "skip-class": { multiple: true, type: "string" },
91
+ "skip-tag": { multiple: true, type: "string" },
92
+ "stdin-filepath": { type: "string" },
93
+ "strong-marker": { type: "string" },
94
+ superscript: { type: "boolean" },
95
+ type: { type: "string" },
96
+ version: { short: "V", type: "boolean" },
97
+ write: { type: "boolean" },
98
+ } as const
99
+
100
+ const HELP_TEXT = `Usage: punctilio [options] [files...]
101
+
102
+ Apply typographic improvements to Markdown and HTML files.
103
+
104
+ Arguments:
105
+ files files to format (pass '-' to read from stdin)
106
+
107
+ Options:
108
+ -V, --version show version
109
+ --write rewrite files in place (default prints formatted output to stdout)
110
+ --check exit 1 if any file would change; write nothing
111
+ --config <path> path to a punctilio config file (otherwise auto-discovered)
112
+ --no-config ignore any auto-discovered config file
113
+ --ignore-path <path> path to an ignore file (otherwise .punctilioignore in cwd)
114
+ --no-cache disable the incremental cache (default location node_modules/.cache/punctilio/cache.json)
115
+ --cache-location <path> override the cache file location
116
+ --stdin-filepath <path> treat stdin as if it were this filename (infers type from extension)
117
+ --type <type> force file type (otherwise inferred from extension) (choices: ${FILE_TYPES.join(", ")})
118
+ --punctuation-style <style> quote and punctuation style (choices: ${PUNCTUATION_STYLES.join(", ")})
119
+ --dash-style <style> dash style (choices: ${DASH_STYLES.join(", ")})
120
+ --no-symbols disable symbol transforms (ellipsis, ×, ©, etc.)
121
+ --nbsp enable non-breaking space insertion (default: on for HTML, off for Markdown)
122
+ --no-nbsp disable non-breaking space insertion
123
+ --no-collapse-spaces preserve runs of consecutive spaces
124
+ --no-arrows disable -> → arrow transforms
125
+ --fractions enable 1/2 → ½
126
+ --degrees enable 20 C → 20 °C
127
+ --superscript enable 1st → 1ˢᵗ
128
+ --ligatures enable !? → ⁉
129
+ --emphasis-marker <char> markdown emphasis character (choices: ${EMPHASIS_MARKERS.join(", ")})
130
+ --strong-marker <char> markdown strong emphasis character (choices: ${EMPHASIS_MARKERS.join(", ")})
131
+ --bullet-marker <char> markdown list bullet character (choices: ${BULLET_MARKERS.join(", ")})
132
+ --rule-marker <char> markdown thematic break character (choices: ${RULE_MARKERS.join(", ")})
133
+ --skip-tag <tag> HTML tag whose contents are left alone (repeatable)
134
+ --skip-class <class> HTML class name whose elements are skipped (repeatable)
135
+ --no-fragment parse input as a full HTML document
136
+ -h, --help display help for command
137
+ `
138
+
139
+ function parseRawArgs(args: string[]) {
140
+ try {
141
+ return parseArgs({
142
+ args,
143
+ allowPositionals: true,
144
+ options: PARSE_ARGS_OPTIONS,
145
+ strict: true,
146
+ tokens: true,
102
147
  })
148
+ } catch (err) {
149
+ // parseArgs throws on unknown options and missing option values.
150
+ throw new UsageError((err as Error).message)
151
+ }
152
+ }
153
+
154
+ type ParsedValues = ReturnType<typeof parseRawArgs>["values"]
155
+ type ParsedTokens = ReturnType<typeof parseRawArgs>["tokens"]
156
+
157
+ function parseChoice<T extends string>(
158
+ flag: string,
159
+ value: string | undefined,
160
+ choices: readonly T[],
161
+ ): T | undefined {
162
+ if (value === undefined) return undefined
163
+ if (!(choices as readonly string[]).includes(value)) {
164
+ throw new UsageError(
165
+ `option '${flag}' argument '${value}' is invalid. Allowed choices are ${choices.join(", ")}.`,
166
+ )
167
+ }
168
+ return value as T
169
+ }
170
+
171
+ /** Maps a `--no-x` boolean to the library's `x: false`, or undefined when unset. */
172
+ function negated(noFlag: boolean | undefined): false | undefined {
173
+ return noFlag ? false : undefined
174
+ }
175
+
176
+ /** Returns the name of whichever of `names` appears last on the command line. */
177
+ function lastFlagToken(tokens: ParsedTokens, names: readonly string[]): string | undefined {
178
+ let lastName: string | undefined
179
+ for (const token of tokens) {
180
+ if (token.kind === "option" && names.includes(token.name)) lastName = token.name
181
+ }
182
+ return lastName
183
+ }
184
+
185
+ function buildFlags(values: ParsedValues, tokens: ParsedTokens): ParsedFlags {
186
+ const flags: ParsedFlags = {
187
+ write: values.write,
188
+ check: values.check,
189
+ cache: negated(values["no-cache"]),
190
+ cacheLocation: values["cache-location"],
191
+ ignorePath: values["ignore-path"],
192
+ stdinFilepath: values["stdin-filepath"],
193
+ type: parseChoice("--type", values.type, FILE_TYPES),
194
+ punctuationStyle: parseChoice("--punctuation-style", values["punctuation-style"], PUNCTUATION_STYLES),
195
+ dashStyle: parseChoice("--dash-style", values["dash-style"], DASH_STYLES),
196
+ symbols: negated(values["no-symbols"]),
197
+ collapseSpaces: negated(values["no-collapse-spaces"]),
198
+ arrows: negated(values["no-arrows"]),
199
+ fractions: values.fractions,
200
+ degrees: values.degrees,
201
+ superscript: values.superscript,
202
+ ligatures: values.ligatures,
203
+ emphasisMarker: parseChoice("--emphasis-marker", values["emphasis-marker"], EMPHASIS_MARKERS),
204
+ strongMarker: parseChoice("--strong-marker", values["strong-marker"], EMPHASIS_MARKERS),
205
+ bulletMarker: parseChoice("--bullet-marker", values["bullet-marker"], BULLET_MARKERS),
206
+ ruleMarker: parseChoice("--rule-marker", values["rule-marker"], RULE_MARKERS),
207
+ skipTag: values["skip-tag"],
208
+ skipClass: values["skip-class"],
209
+ fragment: negated(values["no-fragment"]),
210
+ }
211
+ // nbsp is tri-state: --nbsp / --no-nbsp / unset stays undefined so the
212
+ // per-sink default applies (on for HTML, off for Markdown). When both
213
+ // appear, the last one on the command line wins.
214
+ const nbspToken = lastFlagToken(tokens, ["nbsp", "no-nbsp"])
215
+ if (nbspToken !== undefined) flags.nbsp = nbspToken === "nbsp"
216
+ // config is likewise last-one-wins between --config <path> and --no-config.
217
+ const configToken = lastFlagToken(tokens, ["config", "no-config"])
218
+ if (configToken !== undefined) flags.config = configToken === "config" ? values.config : false
219
+ return flags
103
220
  }
104
221
 
105
222
  function inferFileType(path: string, override?: FileType): FileType {
@@ -112,6 +229,12 @@ function inferFileType(path: string, override?: FileType): FileType {
112
229
  )
113
230
  }
114
231
 
232
+ // A config file may legitimately mix markdown-only and HTML-only keys for use
233
+ // across file types; transformContent strips the keys the sink doesn't accept.
234
+ const CLI_CONFIG_KEYS: readonly string[] = [
235
+ ...new Set([...MARKDOWN_OPTION_KEYS, ...HTML_OPTION_KEYS]),
236
+ ]
237
+
115
238
  // Config keys must match library option names (punctuationStyle, skipTags, …),
116
239
  // not the kebab-cased CLI flag names.
117
240
  async function loadConfig(
@@ -123,6 +246,11 @@ async function loadConfig(
123
246
  const explorer = cosmiconfig("punctilio")
124
247
  const result = configPath ? await explorer.load(configPath) : await explorer.search(cwd)
125
248
  if (!result || result.isEmpty) return {}
249
+ try {
250
+ assertKnownOptionKeys(result.config as object, CLI_CONFIG_KEYS, `config file ${result.filepath}`)
251
+ } catch (err) {
252
+ throw new UsageError((err as Error).message)
253
+ }
126
254
  return result.config as CliOptions
127
255
  }
128
256
 
@@ -212,7 +340,7 @@ function buildOptions(flags: ParsedFlags): CliOptions {
212
340
  if (flags.punctuationStyle) opts.punctuationStyle = flags.punctuationStyle
213
341
  if (flags.dashStyle) opts.dashStyle = flags.dashStyle
214
342
  if (flags.symbols === false) opts.symbols = false
215
- if (flags.nbsp === false) opts.nbsp = false
343
+ if (flags.nbsp !== undefined) opts.nbsp = flags.nbsp
216
344
  if (flags.collapseSpaces === false) opts.collapseSpaces = false
217
345
  if (flags.arrows === false) opts.includeArrows = false
218
346
  if (flags.fractions) opts.fractions = true
@@ -229,8 +357,13 @@ function buildOptions(flags: ParsedFlags): CliOptions {
229
357
  return opts
230
358
  }
231
359
 
360
+ // The merged options may carry keys for the other sink (e.g. emphasisMarker
361
+ // alongside skipTags from one shared config); strip the inapplicable ones so
362
+ // each sink's strict key validation only sees keys it understands.
232
363
  async function transformContent(input: string, type: FileType, opts: CliOptions): Promise<string> {
233
- return type === "md" ? transformMarkdown(input, opts) : transformHtml(input, opts)
364
+ return type === "md"
365
+ ? transformMarkdown(input, omitKeys(opts, HTML_ONLY_OPTION_KEYS) as MarkdownOptions)
366
+ : transformHtml(input, omitKeys(opts, MARKDOWN_ONLY_OPTION_KEYS) as HtmlOptions)
234
367
  }
235
368
 
236
369
  // Unified pipelines unconditionally append a trailing newline; undo if original lacked one.
@@ -264,22 +397,18 @@ async function readPackageVersion(): Promise<string> {
264
397
  /** Returns exit code: 0 success, 1 `--check` saw changes, 2 usage error. */
265
398
  export async function runCli(args: string[], io: CliIO): Promise<number> {
266
399
  const version = await readPackageVersion()
267
- const program = buildProgram(version, io)
268
-
269
- try {
270
- program.parse(args, { from: "user" })
271
- } catch (err) {
272
- /* istanbul ignore if -- commander only throws CommanderError from parse */
273
- if (!(err instanceof CommanderError)) throw err
274
- if (err.code === "commander.helpDisplayed" || err.code === "commander.version") return 0
275
- return 2
276
- }
277
-
278
- const flags = program.opts() as ParsedFlags
279
- const positionals = program.args
280
400
 
281
401
  try {
282
- return await runValidated(flags, positionals, io, program, version)
402
+ const { positionals, tokens, values } = parseRawArgs(args)
403
+ if (values.help) {
404
+ io.stdout.write(HELP_TEXT)
405
+ return 0
406
+ }
407
+ if (values.version) {
408
+ io.stdout.write(`${version}\n`)
409
+ return 0
410
+ }
411
+ return await runValidated(buildFlags(values, tokens), positionals, io, version)
283
412
  } catch (err) {
284
413
  if (err instanceof UsageError) {
285
414
  io.stderr.write(`Error: ${err.message}\n`)
@@ -298,7 +427,6 @@ async function runValidated(
298
427
  flags: ParsedFlags,
299
428
  positionals: string[],
300
429
  io: CliIO,
301
- program: Command,
302
430
  version: string,
303
431
  ): Promise<number> {
304
432
  const cwd = process.cwd()
@@ -309,8 +437,15 @@ async function runValidated(
309
437
  )
310
438
  const opts: CliOptions = { ...config, ...buildOptions(flags) }
311
439
  const check = flags.check ?? false
440
+ const write = flags.write ?? false
441
+ if (check && write) {
442
+ throw new UsageError("--write and --check are mutually exclusive.")
443
+ }
312
444
 
313
445
  if (readsStdin(positionals, flags)) {
446
+ if (write) {
447
+ throw new UsageError("--write cannot be used when reading from stdin.")
448
+ }
314
449
  const fileArgs = positionals.filter((p) => p !== "-")
315
450
  if (fileArgs.length > 0) {
316
451
  throw new UsageError("'-' cannot be combined with file arguments.")
@@ -328,14 +463,16 @@ async function runValidated(
328
463
  }
329
464
 
330
465
  if (positionals.length === 0) {
331
- io.stderr.write(program.helpInformation())
466
+ io.stderr.write(HELP_TEXT)
332
467
  return 2
333
468
  }
334
469
 
335
470
  const ig = loadIgnore(cwd, flags.ignorePath)
336
471
  const files = await discoverFiles(positionals, cwd, ig)
337
472
 
338
- const cacheLocation = flags.cache === false
473
+ // In stdout mode every file's content must be produced, so a cache hit
474
+ // could never skip work; only --write and --check use the cache.
475
+ const cacheLocation = flags.cache === false || (!write && !check)
339
476
  ? undefined
340
477
  : resolve(cwd, flags.cacheLocation ?? DEFAULT_CACHE_LOCATION)
341
478
  const cache = cacheLocation ? loadCache(cacheLocation, io.stderr) : undefined
@@ -357,6 +494,10 @@ async function runValidated(
357
494
  }
358
495
 
359
496
  const formatted = matchTrailingNewline(original, await transformContent(original, type, opts))
497
+ if (!write && !check) {
498
+ io.stdout.write(formatted)
499
+ continue
500
+ }
360
501
  const unchanged = original === formatted
361
502
  if (!unchanged) {
362
503
  anyChanged = true
package/src/dashes.ts CHANGED
@@ -79,10 +79,10 @@ export function enDashNumberRange(text: string, options: DashOptions = {}): stri
79
79
  // as ranges. Space/NNBSP/thin-space grouping isn't captured by the outer
80
80
  // range regex, so those variants currently aren't affected here.
81
81
  if (/^\d{3}$/.test(startNum) && /^\d{4}$/.test(endNum)) return args[0] as string
82
- // Skip US toll-free prefix pattern: 1-800, 1-888, 1-877, etc.
83
- // All US toll-free area codes start with 8 (800, 888, 877, 866, 855, 844, 833).
84
- // We only block 1-8XX to avoid false negatives on legitimate ranges like 1-100.
85
- if (/^1$/.test(startNum) && /^8\d{2}$/.test(endNum)) return args[0] as string
82
+ // Skip US toll-free prefix pattern: 1-800, 1-888, 1-877, etc. Only the
83
+ // seven real toll-free codes (800, 888, 877, 866, 855, 844, 833) are
84
+ // blocked, so genuine ranges like 1-850 or 1-810 still en-dash.
85
+ if (/^1$/.test(startNum) && /^8(?:00|88|77|66|55|44|33)$/.test(endNum)) return args[0] as string
86
86
  return `${groups.precedingAreaCode ?? ""}${groups.start}${EN_DASH}${groups.end}${groups.suffix ?? ""}`
87
87
  }
88
88
  )
package/src/html.ts CHANGED
@@ -3,8 +3,8 @@ import rehypeParse from "rehype-parse"
3
3
  import rehypeStringify from "rehype-stringify"
4
4
  import QuickLRU from "quick-lru"
5
5
 
6
- import { rehypePunctilio, type RehypePunctilioOptions } from "./rehype.js"
7
- import { stableStringify } from "./utils.js"
6
+ import { REHYPE_ONLY_OPTION_KEYS, REHYPE_OPTION_KEYS, rehypePunctilio, type RehypePunctilioOptions } from "./rehype.js"
7
+ import { assertKnownOptionKeys, stableStringify } from "./utils.js"
8
8
 
9
9
  export interface HtmlOptions extends RehypePunctilioOptions {
10
10
  /**
@@ -15,6 +15,13 @@ export interface HtmlOptions extends RehypePunctilioOptions {
15
15
  fragment?: boolean
16
16
  }
17
17
 
18
+ /** Option keys handled by the HTML pipeline (parser plus rehype plugin)
19
+ * rather than `transform()`. */
20
+ export const HTML_ONLY_OPTION_KEYS: readonly string[] = ["fragment", ...REHYPE_ONLY_OPTION_KEYS]
21
+
22
+ /** Runtime list of valid `transformHtml` option keys. */
23
+ export const HTML_OPTION_KEYS: readonly string[] = [...REHYPE_OPTION_KEYS, "fragment"]
24
+
18
25
  function createProcessor(options: HtmlOptions) {
19
26
  const { fragment, ...punctilioOptions } = options
20
27
  return unified()
@@ -38,6 +45,8 @@ export async function transformHtml(
38
45
  input: string,
39
46
  options: HtmlOptions = {}
40
47
  ): Promise<string> {
48
+ assertKnownOptionKeys(options, HTML_OPTION_KEYS, "transformHtml")
49
+
41
50
  // Function-valued options aren't JSON-serializable, so the cache key
42
51
  // can't distinguish them. Bypass the cache to avoid returning a
43
52
  // processor wired to a different shouldSkipText.
package/src/index.ts CHANGED
@@ -139,9 +139,11 @@ export interface TransformOptions {
139
139
  /**
140
140
  * Whether to verify that the transformation is idempotent (running twice
141
141
  * produces the same result). When enabled, throws an error if the second
142
- * pass produces a different result than the first.
142
+ * pass produces a different result than the first. The check doubles the
143
+ * cost of every transform and only detects punctilio's own bugs, so it is
144
+ * off by default; enable it in test suites or when debugging.
143
145
  *
144
- * Default: true
146
+ * Default: false
145
147
  */
146
148
  checkIdempotency?: boolean
147
149
 
@@ -151,7 +153,7 @@ import { niceQuotes } from "./quotes.js"
151
153
  import { hyphenReplace } from "./dashes.js"
152
154
  import { collapseSpaces as collapseSpacesTransform, degrees as degreesTransform, fractions as fractionsTransform, punctuationLigatures as ligaturesTransform, primeMarks, superscriptOrdinal as superscriptTransform, symbolTransform } from "./symbols.js"
153
155
  import { nbspTransform as nbspTransformFn } from "./nbsp.js"
154
- import { assertSeparatorCountPreserved, filterUndefined, formatErrorString } from "./utils.js"
156
+ import { assertKnownOptionKeys, assertSeparatorCountPreserved, filterUndefined, formatErrorString } from "./utils.js"
155
157
  import { DEFAULT_SEPARATOR, ISSUES_URL, UNICODE_SYMBOLS } from "./constants.js"
156
158
 
157
159
  export {
@@ -181,12 +183,21 @@ const defaultOpts: Required<Omit<TransformOptions, "separator">> = {
181
183
  ligatures: false,
182
184
  nbsp: true,
183
185
  collapseSpaces: true,
184
- checkIdempotency: true,
186
+ checkIdempotency: false,
185
187
  punctuationStyle: "american",
186
188
  dashStyle: "american",
187
189
  }
188
190
 
191
+ /** Runtime list of valid `transform()` option keys, derived from the option
192
+ * defaults so it cannot drift from {@link TransformOptions}. */
193
+ export const TRANSFORM_OPTION_KEYS: readonly string[] = [
194
+ ...Object.keys(defaultOpts),
195
+ "separator",
196
+ ]
197
+
189
198
  export function transform(text: string, options: TransformOptions = {}): string {
199
+ assertKnownOptionKeys(options, TRANSFORM_OPTION_KEYS, "transform")
200
+
190
201
  const separator = options.separator ?? DEFAULT_SEPARATOR
191
202
 
192
203
  if (separator.length === 0) {