punctilio 1.10.0 → 2.0.0
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 +0 -2
- package/package.json +1 -23
- package/dist/markdown.d.ts +0 -50
- package/dist/markdown.d.ts.map +0 -1
- package/dist/markdown.js +0 -48
- package/dist/markdown.js.map +0 -1
- package/dist/remark.d.ts +0 -40
- package/dist/remark.d.ts.map +0 -1
- package/dist/remark.js +0 -116
- package/dist/remark.js.map +0 -1
- package/src/markdown.ts +0 -84
- package/src/remark.ts +0 -152
- package/src/tests/markdown.test.ts +0 -71
- package/src/tests/remark.test.ts +0 -173
package/README.md
CHANGED
|
@@ -116,8 +116,6 @@ unified()
|
|
|
116
116
|
// are both resolved correctly across the element boundary.
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
For Markdown ASTs via `remark`, use `remarkPunctilio` which applies the same separator technique to preserve inline element boundaries, or use `transformMarkdown` for a simpler Markdown-to-Markdown pipeline.
|
|
120
|
-
|
|
121
119
|
For manual DOM walking or custom transforms, use `transformElement` from `punctilio/rehype`.
|
|
122
120
|
|
|
123
121
|
## Options
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "punctilio",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Smart typography transformations: curly quotes, em-dashes, en-dashes, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,14 +13,6 @@
|
|
|
13
13
|
"./rehype": {
|
|
14
14
|
"types": "./dist/rehype.d.ts",
|
|
15
15
|
"import": "./dist/rehype.js"
|
|
16
|
-
},
|
|
17
|
-
"./remark": {
|
|
18
|
-
"types": "./dist/remark.d.ts",
|
|
19
|
-
"import": "./dist/remark.js"
|
|
20
|
-
},
|
|
21
|
-
"./markdown": {
|
|
22
|
-
"types": "./dist/markdown.d.ts",
|
|
23
|
-
"import": "./dist/markdown.js"
|
|
24
16
|
}
|
|
25
17
|
},
|
|
26
18
|
"files": [
|
|
@@ -39,8 +31,6 @@
|
|
|
39
31
|
"text-formatting",
|
|
40
32
|
"rehype",
|
|
41
33
|
"rehype-plugin",
|
|
42
|
-
"remark",
|
|
43
|
-
"remark-plugin",
|
|
44
34
|
"unified",
|
|
45
35
|
"markdown",
|
|
46
36
|
"html"
|
|
@@ -62,7 +52,6 @@
|
|
|
62
52
|
"@eslint/js": "^9.39.2",
|
|
63
53
|
"@types/hast": "^3.0.4",
|
|
64
54
|
"@types/jest": "^29.5.12",
|
|
65
|
-
"@types/mdast": "4.0.4",
|
|
66
55
|
"@types/node": "^20.11.0",
|
|
67
56
|
"eslint": "^9.39.2",
|
|
68
57
|
"eslint-plugin-regexp": "^3.0.0",
|
|
@@ -70,9 +59,6 @@
|
|
|
70
59
|
"jest": "^29.7.0",
|
|
71
60
|
"rehype-parse": "9.0.1",
|
|
72
61
|
"rehype-stringify": "10.0.1",
|
|
73
|
-
"remark-gfm": "^4.0.1",
|
|
74
|
-
"remark-parse": "11.0.0",
|
|
75
|
-
"remark-stringify": "11.0.0",
|
|
76
62
|
"retext": "9.0.0",
|
|
77
63
|
"retext-smartypants": "6.2.0",
|
|
78
64
|
"smartquotes": "2.3.2",
|
|
@@ -85,19 +71,11 @@
|
|
|
85
71
|
"unified": "11.0.5"
|
|
86
72
|
},
|
|
87
73
|
"peerDependencies": {
|
|
88
|
-
"remark-parse": "^11.0.0",
|
|
89
|
-
"remark-stringify": "^11.0.0",
|
|
90
74
|
"unified": "^11.0.0"
|
|
91
75
|
},
|
|
92
76
|
"peerDependenciesMeta": {
|
|
93
77
|
"unified": {
|
|
94
78
|
"optional": true
|
|
95
|
-
},
|
|
96
|
-
"remark-parse": {
|
|
97
|
-
"optional": true
|
|
98
|
-
},
|
|
99
|
-
"remark-stringify": {
|
|
100
|
-
"optional": true
|
|
101
79
|
}
|
|
102
80
|
},
|
|
103
81
|
"dependencies": {
|
package/dist/markdown.d.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convenience module for Markdown-to-Markdown typography transformations.
|
|
3
|
-
*
|
|
4
|
-
* Provides a single function that takes Markdown input and returns
|
|
5
|
-
* typographically improved Markdown output.
|
|
6
|
-
*
|
|
7
|
-
* Requires `unified`, `remark-parse`, and `remark-stringify` to be installed.
|
|
8
|
-
*
|
|
9
|
-
* @packageDocumentation
|
|
10
|
-
*/
|
|
11
|
-
import { type RemarkPunctilioOptions } from "./remark.js";
|
|
12
|
-
/**
|
|
13
|
-
* Options for the Markdown transform pipeline.
|
|
14
|
-
*/
|
|
15
|
-
export interface MarkdownOptions extends RemarkPunctilioOptions {
|
|
16
|
-
/**
|
|
17
|
-
* Character for emphasis (`*text*` vs `_text_`) and strong (`**text**` vs `__text__`).
|
|
18
|
-
* Default: "*"
|
|
19
|
-
*/
|
|
20
|
-
emphasisMarker?: "*" | "_";
|
|
21
|
-
/**
|
|
22
|
-
* Character for strong emphasis (`**text**` vs `__text__`). Must match emphasisMarker.
|
|
23
|
-
* Default: "*"
|
|
24
|
-
*/
|
|
25
|
-
strongMarker?: "*" | "_";
|
|
26
|
-
/** Bullet marker for output consistency. Default: "-" */
|
|
27
|
-
bulletMarker?: "-" | "*" | "+";
|
|
28
|
-
/** Thematic break marker. Default: "-" */
|
|
29
|
-
ruleMarker?: "-" | "*" | "_";
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Transforms Markdown text with typographic improvements.
|
|
33
|
-
*
|
|
34
|
-
* Parses the input as Markdown, applies punctilio typography transformations
|
|
35
|
-
* (smart quotes, em-dashes, ellipses, etc.), and serializes back to Markdown.
|
|
36
|
-
*
|
|
37
|
-
* @param input - The Markdown text to transform
|
|
38
|
-
* @param options - Configuration options for both the transform and serializer
|
|
39
|
-
* @returns The typographically improved Markdown text
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* ```ts
|
|
43
|
-
* import { transformMarkdown } from 'punctilio/markdown'
|
|
44
|
-
*
|
|
45
|
-
* await transformMarkdown('"Hello" -- world')
|
|
46
|
-
* // → '\u201CHello\u201D\u2014world'
|
|
47
|
-
* ```
|
|
48
|
-
*/
|
|
49
|
-
export declare function transformMarkdown(input: string, options?: MarkdownOptions): Promise<string>;
|
|
50
|
-
//# sourceMappingURL=markdown.d.ts.map
|
package/dist/markdown.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,EAAmB,KAAK,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAE1E;;GAEG;AACH,MAAM,WAAW,eAAgB,SAAQ,sBAAsB;IAC7D;;;OAGG;IACH,cAAc,CAAC,EAAE,GAAG,GAAG,GAAG,CAAA;IAE1B;;;OAGG;IACH,YAAY,CAAC,EAAE,GAAG,GAAG,GAAG,CAAA;IAExB,yDAAyD;IACzD,YAAY,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;IAE9B,0CAA0C;IAC1C,UAAU,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;CAC7B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAsBjB"}
|
package/dist/markdown.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convenience module for Markdown-to-Markdown typography transformations.
|
|
3
|
-
*
|
|
4
|
-
* Provides a single function that takes Markdown input and returns
|
|
5
|
-
* typographically improved Markdown output.
|
|
6
|
-
*
|
|
7
|
-
* Requires `unified`, `remark-parse`, and `remark-stringify` to be installed.
|
|
8
|
-
*
|
|
9
|
-
* @packageDocumentation
|
|
10
|
-
*/
|
|
11
|
-
import { unified } from "unified";
|
|
12
|
-
import remarkParse from "remark-parse";
|
|
13
|
-
import remarkStringify from "remark-stringify";
|
|
14
|
-
import { remarkPunctilio } from "./remark.js";
|
|
15
|
-
/**
|
|
16
|
-
* Transforms Markdown text with typographic improvements.
|
|
17
|
-
*
|
|
18
|
-
* Parses the input as Markdown, applies punctilio typography transformations
|
|
19
|
-
* (smart quotes, em-dashes, ellipses, etc.), and serializes back to Markdown.
|
|
20
|
-
*
|
|
21
|
-
* @param input - The Markdown text to transform
|
|
22
|
-
* @param options - Configuration options for both the transform and serializer
|
|
23
|
-
* @returns The typographically improved Markdown text
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```ts
|
|
27
|
-
* import { transformMarkdown } from 'punctilio/markdown'
|
|
28
|
-
*
|
|
29
|
-
* await transformMarkdown('"Hello" -- world')
|
|
30
|
-
* // → '\u201CHello\u201D\u2014world'
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
export async function transformMarkdown(input, options = {}) {
|
|
34
|
-
const { emphasisMarker, strongMarker, bulletMarker, ruleMarker, ...punctilioOptions } = options;
|
|
35
|
-
const processor = unified()
|
|
36
|
-
.use(remarkParse)
|
|
37
|
-
.use(remarkPunctilio, punctilioOptions)
|
|
38
|
-
.use(remarkStringify, {
|
|
39
|
-
emphasis: emphasisMarker ?? "*",
|
|
40
|
-
strong: strongMarker ?? "*",
|
|
41
|
-
bullet: bulletMarker ?? "-",
|
|
42
|
-
rule: ruleMarker ?? "-",
|
|
43
|
-
...(ruleMarker === "-" || ruleMarker === undefined ? { ruleRepetition: 4 } : {}),
|
|
44
|
-
});
|
|
45
|
-
const result = await processor.process(input);
|
|
46
|
-
return String(result);
|
|
47
|
-
}
|
|
48
|
-
//# sourceMappingURL=markdown.js.map
|
package/dist/markdown.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,WAAW,MAAM,cAAc,CAAA;AACtC,OAAO,eAAe,MAAM,kBAAkB,CAAA;AAE9C,OAAO,EAAE,eAAe,EAA+B,MAAM,aAAa,CAAA;AAyB1E;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAa,EACb,UAA2B,EAAE;IAE7B,MAAM,EACJ,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,GAAG,gBAAgB,EACpB,GAAG,OAAO,CAAA;IAEX,MAAM,SAAS,GAAG,OAAO,EAAE;SACxB,GAAG,CAAC,WAAW,CAAC;SAChB,GAAG,CAAC,eAAe,EAAE,gBAAgB,CAAC;SACtC,GAAG,CAAC,eAAe,EAAE;QACpB,QAAQ,EAAE,cAAc,IAAI,GAAG;QAC/B,MAAM,EAAE,YAAY,IAAI,GAAG;QAC3B,MAAM,EAAE,YAAY,IAAI,GAAG;QAC3B,IAAI,EAAE,UAAU,IAAI,GAAG;QACvB,GAAG,CAAC,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACjF,CAAC,CAAA;IAEJ,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IAC7C,OAAO,MAAM,CAAC,MAAM,CAAC,CAAA;AACvB,CAAC"}
|
package/dist/remark.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remark plugin for applying punctilio typography transformations to Markdown.
|
|
3
|
-
*
|
|
4
|
-
* This plugin integrates punctilio with the unified/remark ecosystem,
|
|
5
|
-
* allowing you to apply smart typography transformations to Markdown ASTs.
|
|
6
|
-
*
|
|
7
|
-
* @packageDocumentation
|
|
8
|
-
*/
|
|
9
|
-
import type { Root } from "mdast";
|
|
10
|
-
import type { Transformer } from "unified";
|
|
11
|
-
import { type TransformOptions } from "./index.js";
|
|
12
|
-
export type RemarkPunctilioOptions = TransformOptions;
|
|
13
|
-
/**
|
|
14
|
-
* Remark plugin that applies punctilio typography transformations to Markdown.
|
|
15
|
-
*
|
|
16
|
-
* @param options - Plugin configuration options
|
|
17
|
-
* @returns Unified transformer function
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```ts
|
|
21
|
-
* import { unified } from 'unified'
|
|
22
|
-
* import remarkParse from 'remark-parse'
|
|
23
|
-
* import remarkStringify from 'remark-stringify'
|
|
24
|
-
* import remarkPunctilio from 'punctilio/remark'
|
|
25
|
-
*
|
|
26
|
-
* const result = await unified()
|
|
27
|
-
* .use(remarkParse)
|
|
28
|
-
* .use(remarkPunctilio, {
|
|
29
|
-
* punctuationStyle: 'american',
|
|
30
|
-
* dashStyle: 'american',
|
|
31
|
-
* })
|
|
32
|
-
* .use(remarkStringify)
|
|
33
|
-
* .process('"Hello," she said -- "it\'s nice."')
|
|
34
|
-
*
|
|
35
|
-
* // Output Markdown will have smart quotes and em-dashes
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
export declare function remarkPunctilio(options?: RemarkPunctilioOptions): Transformer<Root, Root>;
|
|
39
|
-
export default remarkPunctilio;
|
|
40
|
-
//# sourceMappingURL=remark.d.ts.map
|
package/dist/remark.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"remark.d.ts","sourceRoot":"","sources":["../src/remark.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAyB,MAAM,OAAO,CAAA;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAI1C,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAI7D,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,CAAA;AAqDrD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,GAAE,sBAA2B,GACnC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAmDzB;AAED,eAAe,eAAe,CAAA"}
|
package/dist/remark.js
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remark plugin for applying punctilio typography transformations to Markdown.
|
|
3
|
-
*
|
|
4
|
-
* This plugin integrates punctilio with the unified/remark ecosystem,
|
|
5
|
-
* allowing you to apply smart typography transformations to Markdown ASTs.
|
|
6
|
-
*
|
|
7
|
-
* @packageDocumentation
|
|
8
|
-
*/
|
|
9
|
-
import { visitParents } from "unist-util-visit-parents";
|
|
10
|
-
import { transform } from "./index.js";
|
|
11
|
-
import { DEFAULT_SEPARATOR } from "./constants.js";
|
|
12
|
-
import { formatErrorString } from "./utils.js";
|
|
13
|
-
/**
|
|
14
|
-
* Maximum recursion depth for tree traversal functions.
|
|
15
|
-
* Prevents stack overflow from maliciously deep Markdown nesting.
|
|
16
|
-
*/
|
|
17
|
-
const MAX_RECURSION_DEPTH = 1000;
|
|
18
|
-
/**
|
|
19
|
-
* MDAST node types that contain phrasing (inline) content and should
|
|
20
|
-
* have their text transformed as a single unit.
|
|
21
|
-
*/
|
|
22
|
-
const PHRASING_CONTAINERS = new Set(["paragraph", "heading", "tableCell"]);
|
|
23
|
-
/**
|
|
24
|
-
* MDAST node types whose text content should not be transformed.
|
|
25
|
-
*/
|
|
26
|
-
const SKIP_TYPES = new Set(["inlineCode", "html"]);
|
|
27
|
-
/**
|
|
28
|
-
* Recursively collects text nodes from a phrasing content tree,
|
|
29
|
-
* skipping code and HTML nodes.
|
|
30
|
-
*
|
|
31
|
-
* @param node - The phrasing content node to collect from
|
|
32
|
-
* @param depth - Current recursion depth (internal use)
|
|
33
|
-
* @returns Array of Text nodes
|
|
34
|
-
*/
|
|
35
|
-
function flattenTextNodes(node, depth = 0) {
|
|
36
|
-
/* istanbul ignore if -- defensive: prevents stack overflow from malicious nesting */
|
|
37
|
-
if (depth > MAX_RECURSION_DEPTH) {
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
40
|
-
if (SKIP_TYPES.has(node.type)) {
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
if (node.type === "text") {
|
|
44
|
-
return [node];
|
|
45
|
-
}
|
|
46
|
-
if ("children" in node) {
|
|
47
|
-
return node.children.flatMap((child) => flattenTextNodes(child, depth + 1));
|
|
48
|
-
}
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Remark plugin that applies punctilio typography transformations to Markdown.
|
|
53
|
-
*
|
|
54
|
-
* @param options - Plugin configuration options
|
|
55
|
-
* @returns Unified transformer function
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```ts
|
|
59
|
-
* import { unified } from 'unified'
|
|
60
|
-
* import remarkParse from 'remark-parse'
|
|
61
|
-
* import remarkStringify from 'remark-stringify'
|
|
62
|
-
* import remarkPunctilio from 'punctilio/remark'
|
|
63
|
-
*
|
|
64
|
-
* const result = await unified()
|
|
65
|
-
* .use(remarkParse)
|
|
66
|
-
* .use(remarkPunctilio, {
|
|
67
|
-
* punctuationStyle: 'american',
|
|
68
|
-
* dashStyle: 'american',
|
|
69
|
-
* })
|
|
70
|
-
* .use(remarkStringify)
|
|
71
|
-
* .process('"Hello," she said -- "it\'s nice."')
|
|
72
|
-
*
|
|
73
|
-
* // Output Markdown will have smart quotes and em-dashes
|
|
74
|
-
* ```
|
|
75
|
-
*/
|
|
76
|
-
export function remarkPunctilio(options = {}) {
|
|
77
|
-
const separator = options.separator ?? DEFAULT_SEPARATOR;
|
|
78
|
-
const transformFn = (text) => {
|
|
79
|
-
return transform(text, { ...options, separator });
|
|
80
|
-
};
|
|
81
|
-
return (tree) => {
|
|
82
|
-
visitParents(tree, (node, ancestors) => {
|
|
83
|
-
if (!PHRASING_CONTAINERS.has(node.type)) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
/* istanbul ignore if -- defensive: standard MDAST never nests phrasing containers */
|
|
87
|
-
if (ancestors.some((a) => PHRASING_CONTAINERS.has(a.type))) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
/* istanbul ignore if -- defensive: phrasing containers always have children */
|
|
91
|
-
if (!("children" in node)) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const textNodes = node.children.flatMap((child) => flattenTextNodes(child));
|
|
95
|
-
if (textNodes.length === 0) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
// Same separator technique as the rehype plugin:
|
|
99
|
-
// append marker to each text node, concatenate, transform, split back
|
|
100
|
-
const markedContent = textNodes.map((n) => n.value + separator).join("");
|
|
101
|
-
const transformedContent = transformFn(markedContent);
|
|
102
|
-
const transformedFragments = transformedContent.split(separator).slice(0, -1);
|
|
103
|
-
/* istanbul ignore if -- defensive: transform should never consume separator chars */
|
|
104
|
-
if (transformedFragments.length !== textNodes.length) {
|
|
105
|
-
throw new Error(`Transformation altered the number of text nodes. ` +
|
|
106
|
-
`Expected ${textNodes.length}, got ${transformedFragments.length}. ` +
|
|
107
|
-
`Input: ${formatErrorString(markedContent, "input")}`);
|
|
108
|
-
}
|
|
109
|
-
textNodes.forEach((n, index) => {
|
|
110
|
-
n.value = transformedFragments[index];
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
export default remarkPunctilio;
|
|
116
|
-
//# sourceMappingURL=remark.js.map
|
package/dist/remark.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"remark.js","sourceRoot":"","sources":["../src/remark.ts"],"names":[],"mappings":"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"}
|
package/src/markdown.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convenience module for Markdown-to-Markdown typography transformations.
|
|
3
|
-
*
|
|
4
|
-
* Provides a single function that takes Markdown input and returns
|
|
5
|
-
* typographically improved Markdown output.
|
|
6
|
-
*
|
|
7
|
-
* Requires `unified`, `remark-parse`, and `remark-stringify` to be installed.
|
|
8
|
-
*
|
|
9
|
-
* @packageDocumentation
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { unified } from "unified"
|
|
13
|
-
import remarkParse from "remark-parse"
|
|
14
|
-
import remarkStringify from "remark-stringify"
|
|
15
|
-
|
|
16
|
-
import { remarkPunctilio, type RemarkPunctilioOptions } from "./remark.js"
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Options for the Markdown transform pipeline.
|
|
20
|
-
*/
|
|
21
|
-
export interface MarkdownOptions extends RemarkPunctilioOptions {
|
|
22
|
-
/**
|
|
23
|
-
* Character for emphasis (`*text*` vs `_text_`) and strong (`**text**` vs `__text__`).
|
|
24
|
-
* Default: "*"
|
|
25
|
-
*/
|
|
26
|
-
emphasisMarker?: "*" | "_"
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Character for strong emphasis (`**text**` vs `__text__`). Must match emphasisMarker.
|
|
30
|
-
* Default: "*"
|
|
31
|
-
*/
|
|
32
|
-
strongMarker?: "*" | "_"
|
|
33
|
-
|
|
34
|
-
/** Bullet marker for output consistency. Default: "-" */
|
|
35
|
-
bulletMarker?: "-" | "*" | "+"
|
|
36
|
-
|
|
37
|
-
/** Thematic break marker. Default: "-" */
|
|
38
|
-
ruleMarker?: "-" | "*" | "_"
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Transforms Markdown text with typographic improvements.
|
|
43
|
-
*
|
|
44
|
-
* Parses the input as Markdown, applies punctilio typography transformations
|
|
45
|
-
* (smart quotes, em-dashes, ellipses, etc.), and serializes back to Markdown.
|
|
46
|
-
*
|
|
47
|
-
* @param input - The Markdown text to transform
|
|
48
|
-
* @param options - Configuration options for both the transform and serializer
|
|
49
|
-
* @returns The typographically improved Markdown text
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* ```ts
|
|
53
|
-
* import { transformMarkdown } from 'punctilio/markdown'
|
|
54
|
-
*
|
|
55
|
-
* await transformMarkdown('"Hello" -- world')
|
|
56
|
-
* // → '\u201CHello\u201D\u2014world'
|
|
57
|
-
* ```
|
|
58
|
-
*/
|
|
59
|
-
export async function transformMarkdown(
|
|
60
|
-
input: string,
|
|
61
|
-
options: MarkdownOptions = {}
|
|
62
|
-
): Promise<string> {
|
|
63
|
-
const {
|
|
64
|
-
emphasisMarker,
|
|
65
|
-
strongMarker,
|
|
66
|
-
bulletMarker,
|
|
67
|
-
ruleMarker,
|
|
68
|
-
...punctilioOptions
|
|
69
|
-
} = options
|
|
70
|
-
|
|
71
|
-
const processor = unified()
|
|
72
|
-
.use(remarkParse)
|
|
73
|
-
.use(remarkPunctilio, punctilioOptions)
|
|
74
|
-
.use(remarkStringify, {
|
|
75
|
-
emphasis: emphasisMarker ?? "*",
|
|
76
|
-
strong: strongMarker ?? "*",
|
|
77
|
-
bullet: bulletMarker ?? "-",
|
|
78
|
-
rule: ruleMarker ?? "-",
|
|
79
|
-
...(ruleMarker === "-" || ruleMarker === undefined ? { ruleRepetition: 4 } : {}),
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
const result = await processor.process(input)
|
|
83
|
-
return String(result)
|
|
84
|
-
}
|
package/src/remark.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remark plugin for applying punctilio typography transformations to Markdown.
|
|
3
|
-
*
|
|
4
|
-
* This plugin integrates punctilio with the unified/remark ecosystem,
|
|
5
|
-
* allowing you to apply smart typography transformations to Markdown ASTs.
|
|
6
|
-
*
|
|
7
|
-
* @packageDocumentation
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { Root, PhrasingContent, Text } from "mdast"
|
|
11
|
-
import type { Transformer } from "unified"
|
|
12
|
-
|
|
13
|
-
import { visitParents } from "unist-util-visit-parents"
|
|
14
|
-
|
|
15
|
-
import { transform, type TransformOptions } from "./index.js"
|
|
16
|
-
import { DEFAULT_SEPARATOR } from "./constants.js"
|
|
17
|
-
import { formatErrorString } from "./utils.js"
|
|
18
|
-
|
|
19
|
-
export type RemarkPunctilioOptions = TransformOptions
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Maximum recursion depth for tree traversal functions.
|
|
23
|
-
* Prevents stack overflow from maliciously deep Markdown nesting.
|
|
24
|
-
*/
|
|
25
|
-
const MAX_RECURSION_DEPTH = 1000
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* MDAST node types that contain phrasing (inline) content and should
|
|
29
|
-
* have their text transformed as a single unit.
|
|
30
|
-
*/
|
|
31
|
-
const PHRASING_CONTAINERS = new Set(["paragraph", "heading", "tableCell"])
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* MDAST node types whose text content should not be transformed.
|
|
35
|
-
*/
|
|
36
|
-
const SKIP_TYPES = new Set(["inlineCode", "html"])
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Recursively collects text nodes from a phrasing content tree,
|
|
40
|
-
* skipping code and HTML nodes.
|
|
41
|
-
*
|
|
42
|
-
* @param node - The phrasing content node to collect from
|
|
43
|
-
* @param depth - Current recursion depth (internal use)
|
|
44
|
-
* @returns Array of Text nodes
|
|
45
|
-
*/
|
|
46
|
-
function flattenTextNodes(
|
|
47
|
-
node: PhrasingContent,
|
|
48
|
-
depth: number = 0
|
|
49
|
-
): Text[] {
|
|
50
|
-
/* istanbul ignore if -- defensive: prevents stack overflow from malicious nesting */
|
|
51
|
-
if (depth > MAX_RECURSION_DEPTH) {
|
|
52
|
-
return []
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (SKIP_TYPES.has(node.type)) {
|
|
56
|
-
return []
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (node.type === "text") {
|
|
60
|
-
return [node]
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if ("children" in node) {
|
|
64
|
-
return (node.children as PhrasingContent[]).flatMap((child) =>
|
|
65
|
-
flattenTextNodes(child, depth + 1)
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return []
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Remark plugin that applies punctilio typography transformations to Markdown.
|
|
74
|
-
*
|
|
75
|
-
* @param options - Plugin configuration options
|
|
76
|
-
* @returns Unified transformer function
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* ```ts
|
|
80
|
-
* import { unified } from 'unified'
|
|
81
|
-
* import remarkParse from 'remark-parse'
|
|
82
|
-
* import remarkStringify from 'remark-stringify'
|
|
83
|
-
* import remarkPunctilio from 'punctilio/remark'
|
|
84
|
-
*
|
|
85
|
-
* const result = await unified()
|
|
86
|
-
* .use(remarkParse)
|
|
87
|
-
* .use(remarkPunctilio, {
|
|
88
|
-
* punctuationStyle: 'american',
|
|
89
|
-
* dashStyle: 'american',
|
|
90
|
-
* })
|
|
91
|
-
* .use(remarkStringify)
|
|
92
|
-
* .process('"Hello," she said -- "it\'s nice."')
|
|
93
|
-
*
|
|
94
|
-
* // Output Markdown will have smart quotes and em-dashes
|
|
95
|
-
* ```
|
|
96
|
-
*/
|
|
97
|
-
export function remarkPunctilio(
|
|
98
|
-
options: RemarkPunctilioOptions = {}
|
|
99
|
-
): Transformer<Root, Root> {
|
|
100
|
-
const separator = options.separator ?? DEFAULT_SEPARATOR
|
|
101
|
-
|
|
102
|
-
const transformFn = (text: string): string => {
|
|
103
|
-
return transform(text, { ...options, separator })
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return (tree: Root) => {
|
|
107
|
-
visitParents(tree, (node, ancestors) => {
|
|
108
|
-
if (!PHRASING_CONTAINERS.has(node.type)) {
|
|
109
|
-
return
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/* istanbul ignore if -- defensive: standard MDAST never nests phrasing containers */
|
|
113
|
-
if (ancestors.some((a) => PHRASING_CONTAINERS.has(a.type))) {
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/* istanbul ignore if -- defensive: phrasing containers always have children */
|
|
118
|
-
if (!("children" in node)) {
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const textNodes = (node.children as PhrasingContent[]).flatMap((child) =>
|
|
123
|
-
flattenTextNodes(child)
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
if (textNodes.length === 0) {
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Same separator technique as the rehype plugin:
|
|
131
|
-
// append marker to each text node, concatenate, transform, split back
|
|
132
|
-
const markedContent = textNodes.map((n) => n.value + separator).join("")
|
|
133
|
-
const transformedContent = transformFn(markedContent)
|
|
134
|
-
const transformedFragments = transformedContent.split(separator).slice(0, -1)
|
|
135
|
-
|
|
136
|
-
/* istanbul ignore if -- defensive: transform should never consume separator chars */
|
|
137
|
-
if (transformedFragments.length !== textNodes.length) {
|
|
138
|
-
throw new Error(
|
|
139
|
-
`Transformation altered the number of text nodes. ` +
|
|
140
|
-
`Expected ${textNodes.length}, got ${transformedFragments.length}. ` +
|
|
141
|
-
`Input: ${formatErrorString(markedContent, "input")}`
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
textNodes.forEach((n, index) => {
|
|
146
|
-
n.value = transformedFragments[index]
|
|
147
|
-
})
|
|
148
|
-
})
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export default remarkPunctilio
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { transformMarkdown } from "../markdown.js"
|
|
2
|
-
import { UNICODE_SYMBOLS } from "../constants.js"
|
|
3
|
-
|
|
4
|
-
const {
|
|
5
|
-
LEFT_DOUBLE_QUOTE: LDQ,
|
|
6
|
-
RIGHT_DOUBLE_QUOTE: RDQ,
|
|
7
|
-
RIGHT_SINGLE_QUOTE: RSQ,
|
|
8
|
-
EM_DASH,
|
|
9
|
-
ELLIPSIS,
|
|
10
|
-
} = UNICODE_SYMBOLS
|
|
11
|
-
|
|
12
|
-
describe("transformMarkdown", () => {
|
|
13
|
-
it("transforms basic typography", async () => {
|
|
14
|
-
const result = await transformMarkdown('"Hello," she said.', { nbsp: false })
|
|
15
|
-
expect(result.trimEnd()).toEqual(`${LDQ}Hello,${RDQ} she said.`)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it("preserves code blocks", async () => {
|
|
19
|
-
const input = '```\n"untouched"\n```'
|
|
20
|
-
const result = await transformMarkdown(input, { nbsp: false })
|
|
21
|
-
expect(result.trimEnd()).toEqual(input)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it("handles complex documents", async () => {
|
|
25
|
-
const input = [
|
|
26
|
-
'# "Welcome"',
|
|
27
|
-
"",
|
|
28
|
-
"It's great -- isn't it?",
|
|
29
|
-
"",
|
|
30
|
-
"Wait...",
|
|
31
|
-
].join("\n")
|
|
32
|
-
|
|
33
|
-
const expected = [
|
|
34
|
-
`# ${LDQ}Welcome${RDQ}`,
|
|
35
|
-
"",
|
|
36
|
-
`It${RSQ}s great${EM_DASH}isn${RSQ}t it?`,
|
|
37
|
-
"",
|
|
38
|
-
`Wait${ELLIPSIS}`,
|
|
39
|
-
"", // remark-stringify adds trailing newline
|
|
40
|
-
].join("\n")
|
|
41
|
-
|
|
42
|
-
expect(await transformMarkdown(input, { nbsp: false })).toEqual(expected)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it("passes through punctuationStyle option", async () => {
|
|
46
|
-
const american = await transformMarkdown('"Hello."', {
|
|
47
|
-
punctuationStyle: "american",
|
|
48
|
-
nbsp: false,
|
|
49
|
-
})
|
|
50
|
-
const british = await transformMarkdown('"Hello."', {
|
|
51
|
-
punctuationStyle: "british",
|
|
52
|
-
nbsp: false,
|
|
53
|
-
})
|
|
54
|
-
expect(american.trimEnd()).toEqual(`${LDQ}Hello.${RDQ}`)
|
|
55
|
-
expect(british.trimEnd()).toEqual(`${LDQ}Hello${RDQ}.`)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it.each([
|
|
59
|
-
["bulletMarker", "- item", { bulletMarker: "+" as const }, "+ item"],
|
|
60
|
-
["emphasisMarker", "*italic*", { emphasisMarker: "_" as const }, "_italic_"],
|
|
61
|
-
["strongMarker", "**bold**", { strongMarker: "_" as const }, "__bold__"],
|
|
62
|
-
["ruleMarker", "---", { ruleMarker: "*" as const }, "***"],
|
|
63
|
-
])("passes through %s option", async (_name, input, options, expected) => {
|
|
64
|
-
expect((await transformMarkdown(input, { ...options, nbsp: false })).trimEnd()).toEqual(expected)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it("uses default options when none provided", async () => {
|
|
68
|
-
const result = await transformMarkdown('"Hello"')
|
|
69
|
-
expect(result.trimEnd()).toEqual(`${LDQ}Hello${RDQ}`)
|
|
70
|
-
})
|
|
71
|
-
})
|
package/src/tests/remark.test.ts
DELETED
|
@@ -1,173 +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"  "world"', `${LDQ}Hello${RDQ}  ${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
|
-
})
|