punctilio 3.7.3 → 3.7.5

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.
package/src/symbols.ts CHANGED
@@ -190,24 +190,28 @@ function contextAwareLegalReplace(
190
190
  })
191
191
  }
192
192
 
193
+ const LEGAL_COPYRIGHT_RE = /\(c\)/gi
194
+ const LEGAL_REGISTERED_RE = /\(r\)/gi
195
+ const LEGAL_TRADEMARK_RE = /\(tm\)/gi
196
+
193
197
  /** Convert (c), (r), (tm) to ©, ®, ™. */
194
198
  export function legalSymbols(text: string, options: SymbolOptions = {}): string {
195
199
  const separator = options.separator ?? DEFAULT_SEPARATOR
196
200
  // (c) → © only with positive copyright evidence (year or "copyright" keyword)
197
201
  // and not in a path context (e.g., example.com/path(c))
198
- text = contextAwareLegalReplace(text, /\(c\)/gi, COPYRIGHT, (before, after) =>
202
+ text = contextAwareLegalReplace(text, LEGAL_COPYRIGHT_RE, COPYRIGHT, (before, after) =>
199
203
  !isPathContext(before) && (/^\s*(?:19|20)\d{2}\b/.test(after) || /\bcopyright\s*$/i.test(before)),
200
204
  separator
201
205
  )
202
206
 
203
207
  // (r) → ® unless in enumeration "(q), (r)", legal citation "(r)(1)", or path context
204
- text = contextAwareLegalReplace(text, /\(r\)/gi, REGISTERED, (before, after) =>
208
+ text = contextAwareLegalReplace(text, LEGAL_REGISTERED_RE, REGISTERED, (before, after) =>
205
209
  !/\([a-z]\)[,;]\s*$/i.test(before) && !/^\(\d/.test(after) && !isPathContext(before),
206
210
  separator
207
211
  )
208
212
 
209
213
  // (tm) → ™ unless in a path context
210
- text = contextAwareLegalReplace(text, /\(tm\)/gi, TRADEMARK, (before) =>
214
+ text = contextAwareLegalReplace(text, LEGAL_TRADEMARK_RE, TRADEMARK, (before) =>
211
215
  !isPathContext(before),
212
216
  separator
213
217
  )
@@ -452,10 +456,12 @@ export function superscriptOrdinal(text: string, options: SymbolOptions = {}): s
452
456
  })
453
457
  }
454
458
 
455
- /** Collapse multiple spaces (including tabs) to single space. Prefers nbsp if any nbsp is present. */
459
+ /** Collapse multiple spaces (including tabs) to single space. Preserves the highest-priority space type present in the run: NBSP > NNBSP > regular. Leading whitespace at the start of a line (after `\n` or start-of-string) is preserved so indented blocks (e.g. HN-style code) survive. */
456
460
  export function collapseSpaces(text: string): string {
457
- return text.replace(cachedRegExp(`[${SPACE_CHARS}]{2,}`, "g"), (match) => {
458
- return match.includes(NBSP) ? NBSP : " "
461
+ return text.replace(cachedRegExp(`(?<=[^\\n${SPACE_CHARS}])[${SPACE_CHARS}]{2,}`, "g"), (match) => {
462
+ if (match.includes(NBSP)) return NBSP
463
+ if (match.includes(UNICODE_SYMBOLS.NNBSP)) return UNICODE_SYMBOLS.NNBSP
464
+ return " "
459
465
  })
460
466
  }
461
467
 
package/src/utils.ts CHANGED
@@ -60,6 +60,7 @@ export function assertSeparatorAbsent(textValues: string[], separator: string):
60
60
  }
61
61
 
62
62
  export function countSeparators(text: string, separator: string = DEFAULT_SEPARATOR): number {
63
+ if (separator.length === 0) return 0
63
64
  let count = 0
64
65
  let index = 0
65
66
  while ((index = text.indexOf(separator, index)) !== -1) {
package/tsconfig.json CHANGED
@@ -16,5 +16,5 @@
16
16
  "isolatedModules": true
17
17
  },
18
18
  "include": ["src/**/*"],
19
- "exclude": ["node_modules", "dist", "**/*.test.ts"]
19
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "src/tests"]
20
20
  }
@@ -1,33 +0,0 @@
1
- /**
2
- * Shared test utilities for punctilio test suites.
3
- */
4
- /**
5
- * Builds a deterministic pseudo-random string of approximately `charCount`
6
- * characters by randomly sampling from FRAGMENTS using a seeded PRNG.
7
- *
8
- * @param charCount - Approximate desired character count
9
- * @param seed - PRNG seed (default: "42")
10
- */
11
- export declare function buildMixedContent(charCount: number, seed?: string): string;
12
- /**
13
- * Asserts that a function scales linearly (not quadratically) with input size.
14
- *
15
- * Compares ms/char at the two largest of 4 input sizes (4x vs 8x of startingN)
16
- * after a JIT warmup pass. For a linear algorithm, doubling input keeps ms/char
17
- * constant (ratio ~1). For a quadratic one, doubling input doubles ms/char (~2).
18
- *
19
- * Default startingN of 5000 produces inputs from ~5K to ~40K chars, which is
20
- * past V8's string allocation overhead inflection point where ms/char stabilizes.
21
- *
22
- * Each trial measures `fn` and a known-linear baseline back-to-back at both
23
- * sizes, then divides `fn`'s 8x/4x ratio by the baseline's. Shared platform
24
- * noise (CPU contention, scheduler jitter) inflates both numerator and
25
- * denominator and cancels out; only `fn`-specific super-linear growth pushes
26
- * the normalized ratio past 1. Min across trials picks the cleanest sample.
27
- *
28
- * @param fn - The function to benchmark
29
- * @param buildInput - Builds an input string of approximately `n` units
30
- * @param startingN - Size parameter for the smallest input (default: 5000)
31
- */
32
- export declare function assertLinearScaling(fn: (input: string) => unknown, buildInput: (n: number) => string, startingN?: number): void;
33
- //# sourceMappingURL=test-helpers.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../../src/tests/test-helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAgCH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,MAAa,GAAG,MAAM,CAQhF;AAUD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,EAC9B,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EACjC,SAAS,GAAE,MAAa,GACvB,IAAI,CAwCN"}
@@ -1,109 +0,0 @@
1
- /**
2
- * Shared test utilities for punctilio test suites.
3
- */
4
- import seedrandom from "seedrandom";
5
- /**
6
- * Fragments covering every transform type: quotes, dashes, apostrophes,
7
- * ellipses, multiplication, math symbols, legal symbols, arrows, primes,
8
- * fractions, degrees, ordinals, nbsp-triggering short words, and plain text.
9
- */
10
- const FRAGMENTS = [
11
- `"Hello," she said. `,
12
- `It's a beautiful day -- isn't it? `,
13
- `Wait... `,
14
- `The temperature was 20 C. `,
15
- `5x5 != 25. `,
16
- `Add 1/2 cup. `,
17
- `He's 5'10" tall. `,
18
- `(c) 2024 Acme Corp. `,
19
- `See p. 42 for details. `,
20
- `Dr. Smith arrived. `,
21
- `The 1st place winner. `,
22
- `Go -> click -> done. `,
23
- `Really?! No way!! `,
24
- `A B C D E F G H. `,
25
- `Pages 10-20 contain important info. `,
26
- `January-March was cold. `,
27
- `The value is -5. `,
28
- `"Don't stop," he said, "keep going." `,
29
- `J. K. Rowling wrote it. `,
30
- `100 km in 2 hrs. `,
31
- ];
32
- /**
33
- * Builds a deterministic pseudo-random string of approximately `charCount`
34
- * characters by randomly sampling from FRAGMENTS using a seeded PRNG.
35
- *
36
- * @param charCount - Approximate desired character count
37
- * @param seed - PRNG seed (default: "42")
38
- */
39
- export function buildMixedContent(charCount, seed = "42") {
40
- const random = seedrandom(seed);
41
- let result = "";
42
- while (result.length < charCount) {
43
- const index = Math.floor(random() * FRAGMENTS.length);
44
- result += FRAGMENTS[index];
45
- }
46
- return result;
47
- }
48
- /**
49
- * Provably linear baseline: a single regex pass that touches every char and
50
- * exercises the same V8 String.replace path the production transforms use.
51
- * Its ms/char ratio is the platform's "what linear looks like right now"
52
- * fingerprint — we divide fn's ratio by it to cancel shared CPU/GC noise.
53
- */
54
- const linearBaseline = (s) => s.replace(/[a-z]/gi, "x");
55
- /**
56
- * Asserts that a function scales linearly (not quadratically) with input size.
57
- *
58
- * Compares ms/char at the two largest of 4 input sizes (4x vs 8x of startingN)
59
- * after a JIT warmup pass. For a linear algorithm, doubling input keeps ms/char
60
- * constant (ratio ~1). For a quadratic one, doubling input doubles ms/char (~2).
61
- *
62
- * Default startingN of 5000 produces inputs from ~5K to ~40K chars, which is
63
- * past V8's string allocation overhead inflection point where ms/char stabilizes.
64
- *
65
- * Each trial measures `fn` and a known-linear baseline back-to-back at both
66
- * sizes, then divides `fn`'s 8x/4x ratio by the baseline's. Shared platform
67
- * noise (CPU contention, scheduler jitter) inflates both numerator and
68
- * denominator and cancels out; only `fn`-specific super-linear growth pushes
69
- * the normalized ratio past 1. Min across trials picks the cleanest sample.
70
- *
71
- * @param fn - The function to benchmark
72
- * @param buildInput - Builds an input string of approximately `n` units
73
- * @param startingN - Size parameter for the smallest input (default: 5000)
74
- */
75
- export function assertLinearScaling(fn, buildInput, startingN = 5000) {
76
- const input4x = buildInput(startingN * 4);
77
- const input8x = buildInput(startingN * 8);
78
- // Warmup both with the largest input so JIT compiles all code paths.
79
- fn(input8x);
80
- linearBaseline(input8x);
81
- const minTrials = 8;
82
- const minElapsedMs = 200;
83
- let bestRatio = Infinity;
84
- let totalElapsed = 0;
85
- let trials = 0;
86
- while (trials < minTrials || totalElapsed < minElapsedMs) {
87
- const startFn4 = performance.now();
88
- fn(input4x);
89
- const fnTime4 = performance.now() - startFn4;
90
- const startBase4 = performance.now();
91
- linearBaseline(input4x);
92
- const baseTime4 = performance.now() - startBase4;
93
- const startFn8 = performance.now();
94
- fn(input8x);
95
- const fnTime8 = performance.now() - startFn8;
96
- const startBase8 = performance.now();
97
- linearBaseline(input8x);
98
- const baseTime8 = performance.now() - startBase8;
99
- const fnRatio = (fnTime8 / input8x.length) / (fnTime4 / input4x.length);
100
- const baseRatio = (baseTime8 / input8x.length) / (baseTime4 / input4x.length);
101
- const ratio = fnRatio / baseRatio;
102
- bestRatio = Math.min(bestRatio, ratio);
103
- totalElapsed += fnTime4 + fnTime8 + baseTime4 + baseTime8;
104
- trials++;
105
- }
106
- // After baseline-normalization: linear ≈ 1, quadratic ≈ 2.
107
- expect(bestRatio).toBeLessThan(1.5);
108
- }
109
- //# sourceMappingURL=test-helpers.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../../src/tests/test-helpers.ts"],"names":[],"mappings":"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"}