punctilio 3.7.2 → 3.7.4
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 +28 -28
- package/dist/dashes.d.ts.map +1 -1
- package/dist/dashes.js +2 -1
- package/dist/dashes.js.map +1 -1
- package/dist/rehype.d.ts.map +1 -1
- package/dist/rehype.js +5 -20
- package/dist/rehype.js.map +1 -1
- package/dist/symbols.d.ts +1 -1
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +12 -5
- package/dist/symbols.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -0
- package/dist/utils.js.map +1 -1
- package/package.json +5 -4
- package/src/dashes.ts +2 -1
- package/src/rehype.ts +5 -24
- package/src/symbols.ts +11 -5
- package/src/utils.ts +1 -0
- package/tsconfig.json +1 -1
- package/dist/tests/test-helpers.d.ts +0 -33
- package/dist/tests/test-helpers.d.ts.map +0 -1
- package/dist/tests/test-helpers.js +0 -109
- package/dist/tests/test-helpers.js.map +0 -1
- package/src/tests/dashes.test.ts +0 -736
- package/src/tests/index.test.ts +0 -713
- package/src/tests/markdown.test.ts +0 -156
- package/src/tests/nbsp.test.ts +0 -272
- package/src/tests/quotes.test.ts +0 -783
- package/src/tests/rehype.test.ts +0 -701
- package/src/tests/remark.test.ts +0 -200
- package/src/tests/symbols.test.ts +0 -920
- package/src/tests/test-helpers.ts +0 -124
- package/src/tests/utils.test.ts +0 -151
|
@@ -1,124 +0,0 @@
|
|
|
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
|
-
* Provably linear baseline: a single regex pass that touches every char and
|
|
54
|
-
* exercises the same V8 String.replace path the production transforms use.
|
|
55
|
-
* Its ms/char ratio is the platform's "what linear looks like right now"
|
|
56
|
-
* fingerprint — we divide fn's ratio by it to cancel shared CPU/GC noise.
|
|
57
|
-
*/
|
|
58
|
-
const linearBaseline = (s: string): string => s.replace(/[a-z]/gi, "x")
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Asserts that a function scales linearly (not quadratically) with input size.
|
|
62
|
-
*
|
|
63
|
-
* Compares ms/char at the two largest of 4 input sizes (4x vs 8x of startingN)
|
|
64
|
-
* after a JIT warmup pass. For a linear algorithm, doubling input keeps ms/char
|
|
65
|
-
* constant (ratio ~1). For a quadratic one, doubling input doubles ms/char (~2).
|
|
66
|
-
*
|
|
67
|
-
* Default startingN of 5000 produces inputs from ~5K to ~40K chars, which is
|
|
68
|
-
* past V8's string allocation overhead inflection point where ms/char stabilizes.
|
|
69
|
-
*
|
|
70
|
-
* Each trial measures `fn` and a known-linear baseline back-to-back at both
|
|
71
|
-
* sizes, then divides `fn`'s 8x/4x ratio by the baseline's. Shared platform
|
|
72
|
-
* noise (CPU contention, scheduler jitter) inflates both numerator and
|
|
73
|
-
* denominator and cancels out; only `fn`-specific super-linear growth pushes
|
|
74
|
-
* the normalized ratio past 1. Min across trials picks the cleanest sample.
|
|
75
|
-
*
|
|
76
|
-
* @param fn - The function to benchmark
|
|
77
|
-
* @param buildInput - Builds an input string of approximately `n` units
|
|
78
|
-
* @param startingN - Size parameter for the smallest input (default: 5000)
|
|
79
|
-
*/
|
|
80
|
-
export function assertLinearScaling(
|
|
81
|
-
fn: (input: string) => unknown,
|
|
82
|
-
buildInput: (n: number) => string,
|
|
83
|
-
startingN: number = 5000
|
|
84
|
-
): void {
|
|
85
|
-
const input4x = buildInput(startingN * 4)
|
|
86
|
-
const input8x = buildInput(startingN * 8)
|
|
87
|
-
|
|
88
|
-
// Warmup both with the largest input so JIT compiles all code paths.
|
|
89
|
-
fn(input8x)
|
|
90
|
-
linearBaseline(input8x)
|
|
91
|
-
|
|
92
|
-
const minTrials = 8
|
|
93
|
-
const minElapsedMs = 200
|
|
94
|
-
let bestRatio = Infinity
|
|
95
|
-
let totalElapsed = 0
|
|
96
|
-
let trials = 0
|
|
97
|
-
while (trials < minTrials || totalElapsed < minElapsedMs) {
|
|
98
|
-
const startFn4 = performance.now()
|
|
99
|
-
fn(input4x)
|
|
100
|
-
const fnTime4 = performance.now() - startFn4
|
|
101
|
-
|
|
102
|
-
const startBase4 = performance.now()
|
|
103
|
-
linearBaseline(input4x)
|
|
104
|
-
const baseTime4 = performance.now() - startBase4
|
|
105
|
-
|
|
106
|
-
const startFn8 = performance.now()
|
|
107
|
-
fn(input8x)
|
|
108
|
-
const fnTime8 = performance.now() - startFn8
|
|
109
|
-
|
|
110
|
-
const startBase8 = performance.now()
|
|
111
|
-
linearBaseline(input8x)
|
|
112
|
-
const baseTime8 = performance.now() - startBase8
|
|
113
|
-
|
|
114
|
-
const fnRatio = (fnTime8 / input8x.length) / (fnTime4 / input4x.length)
|
|
115
|
-
const baseRatio = (baseTime8 / input8x.length) / (baseTime4 / input4x.length)
|
|
116
|
-
const ratio = fnRatio / baseRatio
|
|
117
|
-
bestRatio = Math.min(bestRatio, ratio)
|
|
118
|
-
totalElapsed += fnTime4 + fnTime8 + baseTime4 + baseTime8
|
|
119
|
-
trials++
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// After baseline-normalization: linear ≈ 1, quadratic ≈ 2.
|
|
123
|
-
expect(bestRatio).toBeLessThan(1.5)
|
|
124
|
-
}
|
package/src/tests/utils.test.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { jest } from "@jest/globals"
|
|
2
|
-
import { assertSeparatorAbsent, countSeparators, assertSeparatorCountPreserved, formatErrorString } from "../utils.js"
|
|
3
|
-
import { DEFAULT_SEPARATOR, cachedRegExp, clearRegexCache } from "../constants.js"
|
|
4
|
-
|
|
5
|
-
describe("assertSeparatorAbsent", () => {
|
|
6
|
-
it.each([
|
|
7
|
-
["no values", [], DEFAULT_SEPARATOR],
|
|
8
|
-
["clean values", ["hello", "world"], DEFAULT_SEPARATOR],
|
|
9
|
-
])("does not throw: %s", (_desc, values, separator) => {
|
|
10
|
-
expect(() => assertSeparatorAbsent(values, separator)).not.toThrow()
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it.each([
|
|
14
|
-
["default separator", [`hello${DEFAULT_SEPARATOR}world`], DEFAULT_SEPARATOR, /U\+E000 U\+E001/],
|
|
15
|
-
["custom separator", ["hello|world"], "|", /U\+007C/],
|
|
16
|
-
["separator in middle element", ["clean", `has${DEFAULT_SEPARATOR}sep`, "also clean"], DEFAULT_SEPARATOR, /separator sequence/],
|
|
17
|
-
])("throws: %s", (_desc, values, separator, expectedPattern) => {
|
|
18
|
-
expect(() => assertSeparatorAbsent(values, separator)).toThrow(expectedPattern)
|
|
19
|
-
})
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
describe("countSeparators", () => {
|
|
23
|
-
it.each([
|
|
24
|
-
["empty string", "", DEFAULT_SEPARATOR, 0],
|
|
25
|
-
["no separators", "hello world", DEFAULT_SEPARATOR, 0],
|
|
26
|
-
["single separator", `a${DEFAULT_SEPARATOR}b`, DEFAULT_SEPARATOR, 1],
|
|
27
|
-
["multiple separators", `${DEFAULT_SEPARATOR}a${DEFAULT_SEPARATOR}b${DEFAULT_SEPARATOR}`, DEFAULT_SEPARATOR, 3],
|
|
28
|
-
["consecutive separators", `${DEFAULT_SEPARATOR}${DEFAULT_SEPARATOR}${DEFAULT_SEPARATOR}`, DEFAULT_SEPARATOR, 3],
|
|
29
|
-
["custom separator", "a|b|c", "|", 2],
|
|
30
|
-
])("%s: counts correctly", (_desc, input, separator, expected) => {
|
|
31
|
-
expect(countSeparators(input, separator)).toBe(expected)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it.each([
|
|
35
|
-
[`a${DEFAULT_SEPARATOR}b`, 1],
|
|
36
|
-
["no separators", 0],
|
|
37
|
-
[`${DEFAULT_SEPARATOR}${DEFAULT_SEPARATOR}`, 2],
|
|
38
|
-
])("uses DEFAULT_SEPARATOR when omitted: %s -> %i", (input, expected) => {
|
|
39
|
-
expect(countSeparators(input)).toBe(expected)
|
|
40
|
-
})
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
describe("assertSeparatorCountPreserved", () => {
|
|
44
|
-
describe("does not throw when count preserved", () => {
|
|
45
|
-
it.each([
|
|
46
|
-
["same count", `a${DEFAULT_SEPARATOR}b`, `x${DEFAULT_SEPARATOR}y`],
|
|
47
|
-
["zero count", "ab", "xy"],
|
|
48
|
-
["multiple preserved", `${DEFAULT_SEPARATOR}a${DEFAULT_SEPARATOR}b${DEFAULT_SEPARATOR}`, `${DEFAULT_SEPARATOR}x${DEFAULT_SEPARATOR}y${DEFAULT_SEPARATOR}`],
|
|
49
|
-
])("%s", (_desc, original, transformed) => {
|
|
50
|
-
expect(() => assertSeparatorCountPreserved(original, transformed, DEFAULT_SEPARATOR, "test")).not.toThrow()
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
describe("throws when count changes", () => {
|
|
55
|
-
it.each([
|
|
56
|
-
["separator removed", `a${DEFAULT_SEPARATOR}b`, "ab", 1, 0],
|
|
57
|
-
["separator added", "ab", `a${DEFAULT_SEPARATOR}b`, 0, 1],
|
|
58
|
-
["multiple removed", `${DEFAULT_SEPARATOR}${DEFAULT_SEPARATOR}`, "", 2, 0],
|
|
59
|
-
])("%s: expected %i, got %i", (_desc, original, transformed, expectedCount, actualCount) => {
|
|
60
|
-
expect(() => assertSeparatorCountPreserved(original, transformed, DEFAULT_SEPARATOR, "testFn"))
|
|
61
|
-
.toThrow(`testFn altered separator count: expected ${expectedCount}, got ${actualCount}`)
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
describe("default parameters", () => {
|
|
66
|
-
it.each([
|
|
67
|
-
["throws when separator lost", `a${DEFAULT_SEPARATOR}b`, "ab", true],
|
|
68
|
-
["does not throw when preserved", `a${DEFAULT_SEPARATOR}b`, `x${DEFAULT_SEPARATOR}y`, false],
|
|
69
|
-
])("%s", (_desc, original, transformed, shouldThrow) => {
|
|
70
|
-
if (shouldThrow) {
|
|
71
|
-
expect(() => assertSeparatorCountPreserved(original, transformed)).toThrow()
|
|
72
|
-
} else {
|
|
73
|
-
expect(() => assertSeparatorCountPreserved(original, transformed)).not.toThrow()
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
describe("cachedRegExp", () => {
|
|
80
|
-
afterEach(() => {
|
|
81
|
-
clearRegexCache()
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it("evicts entries when cache exceeds max size", () => {
|
|
85
|
-
// Fill the cache well beyond 1000 entries with unique patterns
|
|
86
|
-
for (let i = 0; i < 1010; i++) {
|
|
87
|
-
const re = cachedRegExp(`unique-pattern-${i}`, "g")
|
|
88
|
-
expect(re).toBeInstanceOf(RegExp)
|
|
89
|
-
}
|
|
90
|
-
// Verify the cache still works correctly after eviction
|
|
91
|
-
const re = cachedRegExp("unique-pattern-1009", "g")
|
|
92
|
-
expect(re.source).toBe("unique-pattern-1009")
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
describe("formatErrorString", () => {
|
|
97
|
-
let stderrOutput: string[]
|
|
98
|
-
let stderrSpy: jest.SpiedFunction<typeof process.stderr.write>
|
|
99
|
-
|
|
100
|
-
beforeEach(() => {
|
|
101
|
-
stderrOutput = []
|
|
102
|
-
stderrSpy = jest.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array) => {
|
|
103
|
-
stderrOutput.push(String(chunk))
|
|
104
|
-
return true
|
|
105
|
-
})
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
afterEach(() => {
|
|
109
|
-
stderrSpy.mockRestore()
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it("returns JSON-stringified content for short strings", () => {
|
|
113
|
-
const result = formatErrorString("short text", "test")
|
|
114
|
-
expect(result).toBe('"short text"')
|
|
115
|
-
expect(stderrOutput).toHaveLength(0)
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it("truncates strings over 2000 chars with label and length", () => {
|
|
119
|
-
const longText = "x".repeat(2001)
|
|
120
|
-
const result = formatErrorString(longText, "long-test")
|
|
121
|
-
|
|
122
|
-
expect(result).toContain("[long-test:")
|
|
123
|
-
expect(result).toContain("2001 chars total")
|
|
124
|
-
expect(result).toContain("...")
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it("includes first 2000 chars in truncated output", () => {
|
|
128
|
-
const longText = "a".repeat(1500) + "b".repeat(1000)
|
|
129
|
-
const result = formatErrorString(longText, "mixed")
|
|
130
|
-
|
|
131
|
-
const expected2000 = "a".repeat(1500) + "b".repeat(500)
|
|
132
|
-
expect(result).toContain(JSON.stringify(expected2000))
|
|
133
|
-
expect(result).toContain("2500 chars total")
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it("writes full content to stderr in Node.js for long strings", () => {
|
|
137
|
-
const longText = "z".repeat(2001)
|
|
138
|
-
formatErrorString(longText, "stderr-test")
|
|
139
|
-
|
|
140
|
-
expect(stderrOutput).toHaveLength(1)
|
|
141
|
-
expect(stderrOutput[0]).toContain("punctilio stderr-test full content")
|
|
142
|
-
expect(stderrOutput[0]).toContain(longText)
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it("handles strings at exactly the threshold", () => {
|
|
146
|
-
const exactText = "x".repeat(2000)
|
|
147
|
-
const result = formatErrorString(exactText, "exact")
|
|
148
|
-
expect(result).toBe(JSON.stringify(exactText))
|
|
149
|
-
expect(stderrOutput).toHaveLength(0)
|
|
150
|
-
})
|
|
151
|
-
})
|