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.
@@ -1,200 +0,0 @@
1
- import { unified } from "unified"
2
- import remarkParse from "remark-parse"
3
- import remarkGfm from "remark-gfm"
4
- import remarkStringify from "remark-stringify"
5
- import { remarkPunctilio, type RemarkPunctilioOptions } from "../remark.js"
6
- import { UNICODE_SYMBOLS } from "../constants.js"
7
-
8
- const {
9
- LEFT_DOUBLE_QUOTE: LDQ,
10
- RIGHT_DOUBLE_QUOTE: RDQ,
11
- RIGHT_SINGLE_QUOTE: RSQ,
12
- EM_DASH,
13
- EN_DASH,
14
- ELLIPSIS,
15
- MULTIPLICATION,
16
- NOT_EQUAL,
17
- COPYRIGHT,
18
- NBSP,
19
- FRACTION_1_2,
20
- } = UNICODE_SYMBOLS
21
-
22
- async function processMarkdown(
23
- markdown: string,
24
- options?: RemarkPunctilioOptions
25
- ): Promise<string> {
26
- const result = await unified()
27
- .use(remarkParse)
28
- .use(remarkPunctilio, options)
29
- .use(remarkStringify)
30
- .process(markdown)
31
- return String(result).trimEnd()
32
- }
33
-
34
- async function processGfmMarkdown(
35
- markdown: string,
36
- options?: RemarkPunctilioOptions
37
- ): Promise<string> {
38
- const result = await unified()
39
- .use(remarkParse)
40
- .use(remarkGfm)
41
- .use(remarkPunctilio, options)
42
- .use(remarkStringify)
43
- .process(markdown)
44
- return String(result).trimEnd()
45
- }
46
-
47
- describe("remarkPunctilio", () => {
48
- describe("basic transformations (nbsp enabled)", () => {
49
- it.each([
50
- ["quotes", '"Hello," she said.', `${LDQ}Hello,${RDQ} she${NBSP}said.`],
51
- ["apostrophes", "It's a test.", `It${RSQ}s${NBSP}a${NBSP}test.`],
52
- ["em dashes", "Wait -- here it comes.", `Wait${EM_DASH}here it${NBSP}comes.`],
53
- ["en dashes (number range)", "Pages 1-5", `Pages${NBSP}1${EN_DASH}5`],
54
- ["ellipses", "Wait...", `Wait${ELLIPSIS}`],
55
- ["multiplication", "5x5", `5${MULTIPLICATION}5`],
56
- ["math symbols", "x != y", `x${NBSP}${NOT_EQUAL} y`],
57
- ["legal symbols", "(c) 2024", `${COPYRIGHT}${NBSP}2024`],
58
- ])("transforms %s", async (_name, input, expected) => {
59
- expect(await processMarkdown(input)).toEqual(expected)
60
- })
61
- })
62
-
63
- describe("basic transformations (nbsp disabled)", () => {
64
- it.each([
65
- ["quotes", '"Hello," she said.', `${LDQ}Hello,${RDQ} she said.`],
66
- ["apostrophes", "It's a test.", `It${RSQ}s a test.`],
67
- ["em dashes", "Wait -- here it comes.", `Wait${EM_DASH}here it comes.`],
68
- ["ellipses", "Wait...", `Wait${ELLIPSIS}`],
69
- ["multiplication", "5x5", `5${MULTIPLICATION}5`],
70
- ["math symbols", "x != y", `x ${NOT_EQUAL} y`],
71
- ["legal symbols", "(c) 2024", `${COPYRIGHT} 2024`],
72
- ])("transforms %s", async (_name, input, expected) => {
73
- expect(await processMarkdown(input, { nbsp: false })).toEqual(expected)
74
- })
75
- })
76
-
77
- describe("inline element boundaries", () => {
78
- it.each([
79
- ["quotes spanning emphasis", '"Hello," *she* said.', `${LDQ}Hello,${RDQ} *she* said.`],
80
- ["quotes inside emphasis", '*"Hello,"* she said.', `*${LDQ}Hello,${RDQ}* she said.`],
81
- ["apostrophe in strong", "**It's** fine.", `**It${RSQ}s** fine.`],
82
- ["dash between elements", "word *one* -- *two* word", `word *one*${EM_DASH}*two* word`],
83
- ["deeply nested elements", '*"Hello **beautiful** world"*', `*${LDQ}Hello **beautiful** world${RDQ}*`],
84
- ])("handles %s", async (_name, input, expected) => {
85
- expect(await processMarkdown(input, { nbsp: false })).toEqual(expected)
86
- })
87
- })
88
-
89
- describe("code preservation", () => {
90
- it.each([
91
- ["inline code", '`"Hello" -- test...`'],
92
- ["fenced code blocks", '```\n"Hello" -- test...\n```'],
93
- ["indented code blocks", ' "Hello" -- test...'],
94
- ])("does not transform %s", async (_name, input) => {
95
- const result = await processMarkdown(input, { nbsp: false })
96
- expect(result).toContain('"Hello" -- test...')
97
- expect(result).not.toContain(LDQ)
98
- expect(result).not.toContain(EM_DASH)
99
- expect(result).not.toContain(ELLIPSIS)
100
- })
101
-
102
- it("transforms text alongside inline code", async () => {
103
- expect(await processMarkdown('`code` "Hello"', { nbsp: false }))
104
- .toEqual(`\`code\` ${LDQ}Hello${RDQ}`)
105
- })
106
- })
107
-
108
- describe("element types", () => {
109
- it.each([
110
- ["h1", '# "Hello"', `# ${LDQ}Hello${RDQ}`],
111
- ["h2", '## It\'s nice', `## It${RSQ}s nice`],
112
- ["h3", "### Wait -- really?", `### Wait${EM_DASH}really?`],
113
- ["list items", '- "Hello" -- world', `* ${LDQ}Hello${RDQ}${EM_DASH}world`],
114
- ["blockquotes", '> "Hello," she said.', `> ${LDQ}Hello,${RDQ} she said.`],
115
- ])("transforms %s", async (_name, input, expected) => {
116
- expect(await processMarkdown(input, { nbsp: false })).toEqual(expected)
117
- })
118
-
119
- it.each([
120
- ["nested list items", '- outer "quote"\n - inner "quote"', `* outer ${LDQ}quote${RDQ}\n * inner ${LDQ}quote${RDQ}`],
121
- ["link text", '["Hello," she said.](https://example.com)', `[${LDQ}Hello,${RDQ} she said.](https://example.com)`],
122
- ["text around links", '"Hello" [link](url) "world"', `${LDQ}Hello${RDQ} [link](url) ${LDQ}world${RDQ}`],
123
- ["text around images", '"Hello" ![img](url) "world"', `${LDQ}Hello${RDQ} ![img](url) ${LDQ}world${RDQ}`],
124
- ["inline HTML boundaries", '"Hello"<br>"world"', `${LDQ}Hello${RDQ}<br>${LDQ}world${RDQ}`],
125
- ["hard line breaks", '"Hello" \n"world"', `${LDQ}Hello${RDQ}\\\n${LDQ}world${RDQ}`],
126
- ])("transforms text in %s", async (_name, input, expected) => {
127
- expect(await processMarkdown(input, { nbsp: false })).toEqual(expected)
128
- })
129
- })
130
-
131
- describe("GFM extensions", () => {
132
- it.each([
133
- ["table cells", '| "Hello" | world |\n| --- | --- |\n| "test" | data |', `| ${LDQ}Hello${RDQ} | world |\n| ------- | ----- |\n| ${LDQ}test${RDQ} | data |`],
134
- ["strikethrough", '~~"Hello" -- world~~', `~~${LDQ}Hello${RDQ}${EM_DASH}world~~`],
135
- ])("transforms %s", async (_name, input, expected) => {
136
- expect(await processGfmMarkdown(input, { nbsp: false })).toEqual(expected)
137
- })
138
- })
139
-
140
- describe("transform options passthrough", () => {
141
- it.each([
142
- ["punctuationStyle american", '"Hello."', { punctuationStyle: "american" as const, nbsp: false as const }, `${LDQ}Hello.${RDQ}`],
143
- ["punctuationStyle british", '"Hello."', { punctuationStyle: "british" as const, nbsp: false as const }, `${LDQ}Hello${RDQ}.`],
144
- ["dashStyle american", "word - word", { dashStyle: "american" as const, nbsp: false as const }, `word${EM_DASH}word`],
145
- ["dashStyle british", "word - word", { dashStyle: "british" as const, nbsp: false as const }, `word ${EN_DASH} word`],
146
- ["symbols disabled", "5x5", { symbols: false, nbsp: false as const }, "5x5"],
147
- ["fractions enabled", "1/2 cup", { fractions: true, nbsp: false as const }, `${FRACTION_1_2} cup`],
148
- ["fractions disabled", "1/2 cup", { fractions: false, nbsp: false as const }, "1/2 cup"],
149
- ["custom separator", '"Hello"', { separator: "\uE001", nbsp: false as const }, `${LDQ}Hello${RDQ}`],
150
- ])("respects %s", async (_name, input, options, expected) => {
151
- expect(await processMarkdown(input, options)).toEqual(expected)
152
- })
153
- })
154
-
155
- describe("complex documents", () => {
156
- it("transforms a multi-paragraph document", async () => {
157
- const input = '"Hello," she said.\n\nIt\'s a nice day -- isn\'t it?\n\nWait...'
158
- const expected = `${LDQ}Hello,${RDQ} she said.\n\nIt${RSQ}s a nice day${EM_DASH}isn${RSQ}t it?\n\nWait${ELLIPSIS}`
159
- expect(await processMarkdown(input, { nbsp: false })).toEqual(expected)
160
- })
161
-
162
- it("preserves code blocks among transformed paragraphs", async () => {
163
- const input = '"Transform this"\n\n```\n"Leave this alone"\n```\n\n"Transform this too"'
164
- const expected = `${LDQ}Transform this${RDQ}\n\n\`\`\`\n"Leave this alone"\n\`\`\`\n\n${LDQ}Transform this too${RDQ}`
165
- expect(await processMarkdown(input, { nbsp: false })).toEqual(expected)
166
- })
167
- })
168
-
169
- it("is idempotent", async () => {
170
- const first = await processMarkdown('"Hello," she said -- "it\'s pages 1-5."', { nbsp: false })
171
- expect(await processMarkdown(first, { nbsp: false })).toEqual(first)
172
- })
173
-
174
- describe("stress tests", () => {
175
- it("handles many paragraphs", async () => {
176
- const count = 100
177
- const paragraphs = Array.from({ length: count }, () => '"Hello," she said.').join("\n\n")
178
- const result = await processMarkdown(paragraphs, { nbsp: false })
179
- const matchCount = (result.match(new RegExp(LDQ, "g")) ?? []).length
180
- expect(matchCount).toBe(count)
181
- })
182
-
183
- it("handles paragraph with many inline elements", async () => {
184
- const count = 50
185
- const inlines = Array.from({ length: count }, (_, i) => `*"word${i}"*`).join(" ")
186
- const result = await processMarkdown(inlines, { nbsp: false })
187
- const matchCount = (result.match(new RegExp(LDQ, "g")) ?? []).length
188
- expect(matchCount).toBe(count)
189
- })
190
-
191
- it("handles quotes spanning nested inline elements", async () => {
192
- // Exercises text node flattening across multiple nesting levels:
193
- // emphasis inside strong inside link text
194
- const md = `[**"Hello,** *world"*](url)`
195
- const result = await processMarkdown(md, { nbsp: false })
196
- expect(result).toContain(LDQ)
197
- expect(result).toContain(RDQ)
198
- })
199
- })
200
- })