punctilio 3.2.3 → 3.3.1
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/README.md +4 -3
- package/dist/constants.d.ts +6 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +9 -2
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/nbsp.d.ts.map +1 -1
- package/dist/nbsp.js +5 -1
- package/dist/nbsp.js.map +1 -1
- package/dist/symbols.d.ts +1 -1
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +39 -29
- package/dist/symbols.js.map +1 -1
- package/dist/tests/test-helpers.d.ts +30 -0
- package/dist/tests/test-helpers.d.ts.map +1 -0
- package/dist/tests/test-helpers.js +91 -0
- package/dist/tests/test-helpers.js.map +1 -0
- package/package.json +3 -1
- package/src/constants.ts +10 -2
- package/src/index.ts +8 -0
- package/src/nbsp.ts +5 -1
- package/src/symbols.ts +47 -32
- package/src/tests/dashes.test.ts +11 -0
- package/src/tests/index.test.ts +106 -43
- package/src/tests/markdown.test.ts +24 -0
- package/src/tests/nbsp.test.ts +12 -0
- package/src/tests/quotes.test.ts +30 -12
- package/src/tests/rehype.test.ts +54 -7
- package/src/tests/remark.test.ts +27 -0
- package/src/tests/symbols.test.ts +24 -5
- package/src/tests/test-helpers.ts +102 -0
- package/src/tests/utils.test.ts +11 -7
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test utilities for punctilio test suites.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import seedrandom from "seedrandom"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fragments covering every transform type: quotes, dashes, apostrophes,
|
|
9
|
+
* ellipses, multiplication, math symbols, legal symbols, arrows, primes,
|
|
10
|
+
* fractions, degrees, ordinals, nbsp-triggering short words, and plain text.
|
|
11
|
+
*/
|
|
12
|
+
const FRAGMENTS = [
|
|
13
|
+
`"Hello," she said. `,
|
|
14
|
+
`It's a beautiful day -- isn't it? `,
|
|
15
|
+
`Wait... `,
|
|
16
|
+
`The temperature was 20 C. `,
|
|
17
|
+
`5x5 != 25. `,
|
|
18
|
+
`Add 1/2 cup. `,
|
|
19
|
+
`He's 5'10" tall. `,
|
|
20
|
+
`(c) 2024 Acme Corp. `,
|
|
21
|
+
`See p. 42 for details. `,
|
|
22
|
+
`Dr. Smith arrived. `,
|
|
23
|
+
`The 1st place winner. `,
|
|
24
|
+
`Go -> click -> done. `,
|
|
25
|
+
`Really?! No way!! `,
|
|
26
|
+
`A B C D E F G H. `,
|
|
27
|
+
`Pages 10-20 contain important info. `,
|
|
28
|
+
`January-March was cold. `,
|
|
29
|
+
`The value is -5. `,
|
|
30
|
+
`"Don't stop," he said, "keep going." `,
|
|
31
|
+
`J. K. Rowling wrote it. `,
|
|
32
|
+
`100 km in 2 hrs. `,
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds a deterministic pseudo-random string of approximately `charCount`
|
|
37
|
+
* characters by randomly sampling from FRAGMENTS using a seeded PRNG.
|
|
38
|
+
*
|
|
39
|
+
* @param charCount - Approximate desired character count
|
|
40
|
+
* @param seed - PRNG seed (default: "42")
|
|
41
|
+
*/
|
|
42
|
+
export function buildMixedContent(charCount: number, seed: string = "42"): string {
|
|
43
|
+
const random = seedrandom(seed)
|
|
44
|
+
let result = ""
|
|
45
|
+
while (result.length < charCount) {
|
|
46
|
+
const index = Math.floor(random() * FRAGMENTS.length)
|
|
47
|
+
result += FRAGMENTS[index]
|
|
48
|
+
}
|
|
49
|
+
return result
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Asserts that a function scales linearly (not quadratically) with input size.
|
|
54
|
+
*
|
|
55
|
+
* Measures ms/char at 4 input sizes (1x, 2x, 4x, 8x) after a JIT warmup pass.
|
|
56
|
+
* Compares the two largest sizes (4x vs 8x) which have the best signal-to-noise
|
|
57
|
+
* ratio. For a linear algorithm, doubling input keeps ms/char constant (ratio ~1).
|
|
58
|
+
* For a quadratic one, doubling input doubles ms/char (ratio ~2).
|
|
59
|
+
*
|
|
60
|
+
* Default startingN of 5000 produces inputs from ~5K to ~40K chars, which is
|
|
61
|
+
* past V8's string allocation overhead inflection point where ms/char stabilizes.
|
|
62
|
+
*
|
|
63
|
+
* Uses min 3 iterations and min 20ms per size to get stable averages.
|
|
64
|
+
*
|
|
65
|
+
* @param fn - The function to benchmark
|
|
66
|
+
* @param buildInput - Builds an input string of approximately `n` units
|
|
67
|
+
* @param startingN - Size parameter for the smallest input (default: 5000)
|
|
68
|
+
*/
|
|
69
|
+
export function assertLinearScaling(
|
|
70
|
+
fn: (input: string) => unknown,
|
|
71
|
+
buildInput: (n: number) => string,
|
|
72
|
+
startingN: number = 5000
|
|
73
|
+
): void {
|
|
74
|
+
const multipliers = [1, 2, 4, 8]
|
|
75
|
+
|
|
76
|
+
// Warmup with the largest input so JIT compiles all code paths before timing.
|
|
77
|
+
fn(buildInput(startingN * multipliers[multipliers.length - 1]))
|
|
78
|
+
|
|
79
|
+
function measureMsPerChar(mult: number): number {
|
|
80
|
+
const input = buildInput(startingN * mult)
|
|
81
|
+
const minIterations = 3
|
|
82
|
+
const minElapsedMs = 20
|
|
83
|
+
let totalElapsed = 0
|
|
84
|
+
let iterations = 0
|
|
85
|
+
while (iterations < minIterations || totalElapsed < minElapsedMs) {
|
|
86
|
+
const start = performance.now()
|
|
87
|
+
fn(input)
|
|
88
|
+
totalElapsed += performance.now() - start
|
|
89
|
+
iterations++
|
|
90
|
+
}
|
|
91
|
+
return totalElapsed / iterations / input.length
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Measure all sizes (exercises the function at various scales)
|
|
95
|
+
const msPerChar = multipliers.map(measureMsPerChar)
|
|
96
|
+
|
|
97
|
+
// Compare the two largest inputs (best SNR, per-call overhead is negligible).
|
|
98
|
+
// For linear: rate_8x / rate_4x ≈ 1. For quadratic: ≈ 2.
|
|
99
|
+
const rate4x = msPerChar[2]
|
|
100
|
+
const rate8x = msPerChar[3]
|
|
101
|
+
expect(rate8x / rate4x).toBeLessThan(1.5)
|
|
102
|
+
}
|
package/src/tests/utils.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { jest } from "@jest/globals"
|
|
1
2
|
import { assertSeparatorAbsent, countSeparators, assertSeparatorCountPreserved, formatErrorString } from "../utils.js"
|
|
2
|
-
import { DEFAULT_SEPARATOR, cachedRegExp } from "../constants.js"
|
|
3
|
+
import { DEFAULT_SEPARATOR, cachedRegExp, clearRegexCache } from "../constants.js"
|
|
3
4
|
|
|
4
5
|
describe("assertSeparatorAbsent", () => {
|
|
5
6
|
it.each([
|
|
@@ -76,6 +77,10 @@ describe("assertSeparatorCountPreserved", () => {
|
|
|
76
77
|
})
|
|
77
78
|
|
|
78
79
|
describe("cachedRegExp", () => {
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
clearRegexCache()
|
|
82
|
+
})
|
|
83
|
+
|
|
79
84
|
it("evicts oldest entry when cache exceeds max size", () => {
|
|
80
85
|
// Fill the cache well beyond 1000 entries with unique patterns
|
|
81
86
|
for (let i = 0; i < 1010; i++) {
|
|
@@ -89,20 +94,19 @@ describe("cachedRegExp", () => {
|
|
|
89
94
|
})
|
|
90
95
|
|
|
91
96
|
describe("formatErrorString", () => {
|
|
92
|
-
let originalWrite: typeof process.stderr.write
|
|
93
97
|
let stderrOutput: string[]
|
|
98
|
+
let stderrSpy: jest.SpiedFunction<typeof process.stderr.write>
|
|
94
99
|
|
|
95
100
|
beforeEach(() => {
|
|
96
|
-
originalWrite = process.stderr.write
|
|
97
101
|
stderrOutput = []
|
|
98
|
-
process.stderr.
|
|
99
|
-
stderrOutput.push(chunk)
|
|
102
|
+
stderrSpy = jest.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array) => {
|
|
103
|
+
stderrOutput.push(String(chunk))
|
|
100
104
|
return true
|
|
101
|
-
})
|
|
105
|
+
})
|
|
102
106
|
})
|
|
103
107
|
|
|
104
108
|
afterEach(() => {
|
|
105
|
-
|
|
109
|
+
stderrSpy.mockRestore()
|
|
106
110
|
})
|
|
107
111
|
|
|
108
112
|
it("returns JSON-stringified content for short strings", () => {
|