punctilio 1.3.1 → 1.4.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 +32 -25
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -0
- package/dist/constants.js.map +1 -1
- package/dist/dashes.d.ts.map +1 -1
- package/dist/dashes.js +29 -8
- package/dist/dashes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/quotes.d.ts.map +1 -1
- package/dist/quotes.js +17 -12
- package/dist/quotes.js.map +1 -1
- package/dist/rehype.d.ts +161 -0
- package/dist/rehype.d.ts.map +1 -0
- package/dist/rehype.js +401 -0
- package/dist/rehype.js.map +1 -0
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +2 -1
- package/dist/symbols.js.map +1 -1
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +25 -0
- package/dist/utils.js.map +1 -1
- package/package.json +28 -4
- package/src/constants.ts +161 -0
- package/src/dashes.ts +217 -0
- package/src/index.ts +243 -0
- package/src/quotes.ts +132 -0
- package/src/rehype.ts +503 -0
- package/src/symbols.ts +324 -0
- package/src/tests/dashes.test.ts +653 -0
- package/src/tests/index.test.ts +528 -0
- package/src/tests/quotes.test.ts +344 -0
- package/src/tests/rehype.test.ts +418 -0
- package/src/tests/symbols.test.ts +708 -0
- package/src/tests/utils.test.ts +128 -0
- package/src/utils.ts +72 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { countSeparators, assertSeparatorCountPreserved, formatErrorString } from "../utils.js"
|
|
2
|
+
import { DEFAULT_SEPARATOR } from "../constants.js"
|
|
3
|
+
|
|
4
|
+
describe("countSeparators", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
["empty string", "", DEFAULT_SEPARATOR, 0],
|
|
7
|
+
["no separators", "hello world", DEFAULT_SEPARATOR, 0],
|
|
8
|
+
["single separator", `a${DEFAULT_SEPARATOR}b`, DEFAULT_SEPARATOR, 1],
|
|
9
|
+
["multiple separators", `${DEFAULT_SEPARATOR}a${DEFAULT_SEPARATOR}b${DEFAULT_SEPARATOR}`, DEFAULT_SEPARATOR, 3],
|
|
10
|
+
["consecutive separators", `${DEFAULT_SEPARATOR}${DEFAULT_SEPARATOR}${DEFAULT_SEPARATOR}`, DEFAULT_SEPARATOR, 3],
|
|
11
|
+
["custom separator", "a|b|c", "|", 2],
|
|
12
|
+
])("%s: counts correctly", (_desc, input, separator, expected) => {
|
|
13
|
+
expect(countSeparators(input, separator)).toBe(expected)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it.each([
|
|
17
|
+
[`a${DEFAULT_SEPARATOR}b`, 1],
|
|
18
|
+
["no separators", 0],
|
|
19
|
+
[`${DEFAULT_SEPARATOR}${DEFAULT_SEPARATOR}`, 2],
|
|
20
|
+
])("uses DEFAULT_SEPARATOR when omitted: %s -> %i", (input, expected) => {
|
|
21
|
+
expect(countSeparators(input)).toBe(expected)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe("assertSeparatorCountPreserved", () => {
|
|
26
|
+
describe("does not throw when count preserved", () => {
|
|
27
|
+
it.each([
|
|
28
|
+
["same count", `a${DEFAULT_SEPARATOR}b`, `x${DEFAULT_SEPARATOR}y`],
|
|
29
|
+
["zero count", "ab", "xy"],
|
|
30
|
+
["multiple preserved", `${DEFAULT_SEPARATOR}a${DEFAULT_SEPARATOR}b${DEFAULT_SEPARATOR}`, `${DEFAULT_SEPARATOR}x${DEFAULT_SEPARATOR}y${DEFAULT_SEPARATOR}`],
|
|
31
|
+
])("%s", (_desc, original, transformed) => {
|
|
32
|
+
expect(() => assertSeparatorCountPreserved(original, transformed, DEFAULT_SEPARATOR, "test")).not.toThrow()
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe("throws when count changes", () => {
|
|
37
|
+
it.each([
|
|
38
|
+
["separator removed", `a${DEFAULT_SEPARATOR}b`, "ab", 1, 0],
|
|
39
|
+
["separator added", "ab", `a${DEFAULT_SEPARATOR}b`, 0, 1],
|
|
40
|
+
["multiple removed", `${DEFAULT_SEPARATOR}${DEFAULT_SEPARATOR}`, "", 2, 0],
|
|
41
|
+
])("%s: expected %i, got %i", (_desc, original, transformed, expectedCount, actualCount) => {
|
|
42
|
+
expect(() => assertSeparatorCountPreserved(original, transformed, DEFAULT_SEPARATOR, "testFn"))
|
|
43
|
+
.toThrow(`testFn altered separator count: expected ${expectedCount}, got ${actualCount}`)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe("default parameters", () => {
|
|
48
|
+
it.each([
|
|
49
|
+
["throws when separator lost", `a${DEFAULT_SEPARATOR}b`, "ab", true],
|
|
50
|
+
["does not throw when preserved", `a${DEFAULT_SEPARATOR}b`, `x${DEFAULT_SEPARATOR}y`, false],
|
|
51
|
+
])("%s", (_desc, original, transformed, shouldThrow) => {
|
|
52
|
+
if (shouldThrow) {
|
|
53
|
+
expect(() => assertSeparatorCountPreserved(original, transformed)).toThrow()
|
|
54
|
+
} else {
|
|
55
|
+
expect(() => assertSeparatorCountPreserved(original, transformed)).not.toThrow()
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe("formatErrorString", () => {
|
|
62
|
+
it("returns JSON-stringified content for short strings", () => {
|
|
63
|
+
const result = formatErrorString("short text", "test")
|
|
64
|
+
expect(result).toBe('"short text"')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("truncates strings over 2000 chars with label and length", () => {
|
|
68
|
+
const longText = "x".repeat(2001)
|
|
69
|
+
const result = formatErrorString(longText, "long-test")
|
|
70
|
+
|
|
71
|
+
expect(result).toContain("[long-test:")
|
|
72
|
+
expect(result).toContain("2001 chars total")
|
|
73
|
+
expect(result).toContain("...")
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("includes first 2000 chars in truncated output", () => {
|
|
77
|
+
const longText = "a".repeat(1500) + "b".repeat(1000)
|
|
78
|
+
const result = formatErrorString(longText, "mixed")
|
|
79
|
+
|
|
80
|
+
// Should contain all 1500 a's and first 500 b's (2000 total)
|
|
81
|
+
const expected2000 = "a".repeat(1500) + "b".repeat(500)
|
|
82
|
+
expect(result).toContain(JSON.stringify(expected2000))
|
|
83
|
+
expect(result).toContain("2500 chars total")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("writes full content to stderr in Node.js for long strings", () => {
|
|
87
|
+
const originalWrite = process.stderr.write
|
|
88
|
+
const written: string[] = []
|
|
89
|
+
process.stderr.write = ((chunk: string) => {
|
|
90
|
+
written.push(chunk)
|
|
91
|
+
return true
|
|
92
|
+
}) as typeof process.stderr.write
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const longText = "z".repeat(2001)
|
|
96
|
+
formatErrorString(longText, "stderr-test")
|
|
97
|
+
|
|
98
|
+
expect(written.length).toBe(1)
|
|
99
|
+
expect(written[0]).toContain("punctilio stderr-test full content")
|
|
100
|
+
expect(written[0]).toContain(longText)
|
|
101
|
+
} finally {
|
|
102
|
+
process.stderr.write = originalWrite
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("does not write to stderr for short strings", () => {
|
|
107
|
+
const originalWrite = process.stderr.write
|
|
108
|
+
const written: string[] = []
|
|
109
|
+
process.stderr.write = ((chunk: string) => {
|
|
110
|
+
written.push(chunk)
|
|
111
|
+
return true
|
|
112
|
+
}) as typeof process.stderr.write
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
formatErrorString("short", "test")
|
|
116
|
+
expect(written.length).toBe(0)
|
|
117
|
+
} finally {
|
|
118
|
+
process.stderr.write = originalWrite
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("handles strings at exactly the threshold", () => {
|
|
123
|
+
const exactText = "x".repeat(2000)
|
|
124
|
+
const result = formatErrorString(exactText, "exact")
|
|
125
|
+
// At threshold, should return JSON-stringified (not truncated)
|
|
126
|
+
expect(result).toBe(JSON.stringify(exactText))
|
|
127
|
+
})
|
|
128
|
+
})
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for text transformation validation.
|
|
3
|
+
*
|
|
4
|
+
* @module utils
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_SEPARATOR } from "./constants.js"
|
|
8
|
+
|
|
9
|
+
/** Threshold above which strings are truncated in error messages. */
|
|
10
|
+
const ERROR_STRING_THRESHOLD = 2000
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Formats a string for error messages. If the string exceeds the threshold,
|
|
14
|
+
* truncates it and shows the total length.
|
|
15
|
+
*
|
|
16
|
+
* In Node.js environments, also writes the full content to stderr
|
|
17
|
+
* so it's available in build logs for debugging.
|
|
18
|
+
*/
|
|
19
|
+
export function formatErrorString(content: string, label: string): string {
|
|
20
|
+
if (content.length <= ERROR_STRING_THRESHOLD) {
|
|
21
|
+
return JSON.stringify(content)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// In Node.js, write full content to stderr for debugging
|
|
25
|
+
try {
|
|
26
|
+
if (typeof globalThis.process?.stderr?.write === "function") {
|
|
27
|
+
globalThis.process.stderr.write(
|
|
28
|
+
`\n[punctilio ${label} full content (${content.length} chars)]:\n${content}\n\n`
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Ignore — not in Node.js or stderr unavailable
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const truncated = content.slice(0, ERROR_STRING_THRESHOLD)
|
|
36
|
+
return `[${label}: ${JSON.stringify(truncated)}... (${content.length} chars total)]`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function countSeparators(text: string, separator: string = DEFAULT_SEPARATOR): number {
|
|
40
|
+
let count = 0
|
|
41
|
+
for (const char of text) {
|
|
42
|
+
if (char === separator) count++
|
|
43
|
+
}
|
|
44
|
+
return count
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validates that a transformation preserved the separator count.
|
|
49
|
+
* Throws an error if separators were added or removed.
|
|
50
|
+
*
|
|
51
|
+
* @param original - The original text before transformation
|
|
52
|
+
* @param transformed - The text after transformation
|
|
53
|
+
* @param separator - The separator character to check (default: DEFAULT_SEPARATOR)
|
|
54
|
+
* @param transformName - Name of the transform for error messages (default: "transform")
|
|
55
|
+
* @throws Error if separator count changed
|
|
56
|
+
*/
|
|
57
|
+
export function assertSeparatorCountPreserved(
|
|
58
|
+
original: string,
|
|
59
|
+
transformed: string,
|
|
60
|
+
separator: string = DEFAULT_SEPARATOR,
|
|
61
|
+
transformName: string = "transform"
|
|
62
|
+
): void {
|
|
63
|
+
const originalCount = countSeparators(original, separator)
|
|
64
|
+
const transformedCount = countSeparators(transformed, separator)
|
|
65
|
+
if (originalCount !== transformedCount) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${transformName} altered separator count: expected ${originalCount}, got ${transformedCount}.\n` +
|
|
68
|
+
`This is a bug in punctilio. Please file an issue at https://github.com/alexander-turner/punctilio/issues\n` +
|
|
69
|
+
`Include the input text that caused this error.`
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"isolatedModules": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
20
|
+
}
|