markdown2typst 0.1.3 → 0.1.5
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 +1 -1
- package/dist/markdown2typst.browser.js +20488 -0
- package/dist/markdown2typst.browser.js.map +7 -0
- package/dist/markdown2typst.browser.min.js +105 -0
- package/package.json +1 -1
- package/src/block-renderer.ts +15 -1
- package/src/external-modules.d.ts +36 -0
- package/src/inline-renderer.ts +4 -5
- package/src/markdown2typst.ts +101 -99
- package/src/types.ts +11 -0
- package/src/utils.ts +161 -137
package/src/utils.ts
CHANGED
|
@@ -3,239 +3,263 @@
|
|
|
3
3
|
* @module utils
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import langs from
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
6
|
+
import langs from "langs";
|
|
7
|
+
import type { Langs } from "langs";
|
|
8
|
+
import { getCode, getCodes } from "country-list";
|
|
9
|
+
import type {
|
|
10
|
+
Definition,
|
|
11
|
+
PhrasingContent,
|
|
12
|
+
Text,
|
|
13
|
+
InlineCode,
|
|
14
|
+
Strong,
|
|
15
|
+
Link,
|
|
16
|
+
LinkReference,
|
|
17
|
+
} from "mdast";
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
20
|
* Escape special characters in Typst text content.
|
|
12
21
|
* Escapes characters that have special meaning in Typst markup.
|
|
13
|
-
*
|
|
22
|
+
*
|
|
14
23
|
* @param input - Raw text string
|
|
15
24
|
* @returns Escaped text safe for Typst
|
|
16
25
|
*/
|
|
17
26
|
export function escapeTypstText(input: string): string {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
try {
|
|
28
|
+
return input.replace(/[\\#*_`\[\]\$<>@]/g, (c) => `\\${c}`);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
// Fallback: return original string if escaping fails
|
|
31
|
+
return input;
|
|
32
|
+
}
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
/**
|
|
27
36
|
* Escape special characters in Typst string literals.
|
|
28
37
|
* Used for strings within quotes (URLs, titles, etc.).
|
|
29
|
-
*
|
|
38
|
+
*
|
|
30
39
|
* @param input - Raw string
|
|
31
40
|
* @returns Escaped string safe for Typst string literals
|
|
32
41
|
*/
|
|
33
42
|
export function escapeTypstString(input: string): string {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
try {
|
|
44
|
+
return input
|
|
45
|
+
.replace(/\\/g, "\\\\")
|
|
46
|
+
.replace(/"/g, '\\"')
|
|
47
|
+
.replace(/\n/g, "\\n");
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// Fallback: return original string if escaping fails
|
|
50
|
+
return input;
|
|
51
|
+
}
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
/**
|
|
43
55
|
* Indent all lines of text by a given level.
|
|
44
56
|
* Each indent level adds 2 spaces.
|
|
45
|
-
*
|
|
57
|
+
*
|
|
46
58
|
* @param text - Text to indent
|
|
47
59
|
* @param indentLevel - Number of indent levels (0 = no indent)
|
|
48
60
|
* @returns Indented text
|
|
49
61
|
*/
|
|
50
62
|
export function indentLines(text: string, indentLevel: number): string {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
if (!indentLevel) return text;
|
|
64
|
+
const indent = " ".repeat(indentLevel);
|
|
65
|
+
return text
|
|
66
|
+
.split("\n")
|
|
67
|
+
.map((line) => `${indent}${line}`)
|
|
68
|
+
.join("\n");
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
/**
|
|
60
72
|
* Type guard to check if a value is a non-empty string.
|
|
61
73
|
* Useful for filtering arrays.
|
|
62
|
-
*
|
|
74
|
+
*
|
|
63
75
|
* @param value - Value to check
|
|
64
76
|
* @returns True if value is a non-empty string
|
|
65
77
|
*/
|
|
66
78
|
export function isNonEmpty(value: string | null | undefined): value is string {
|
|
67
|
-
|
|
79
|
+
return typeof value === "string" && value.length > 0;
|
|
68
80
|
}
|
|
69
81
|
|
|
70
82
|
/**
|
|
71
83
|
* Normalize text by trimming whitespace.
|
|
72
|
-
*
|
|
84
|
+
*
|
|
73
85
|
* @param value - Text to normalize
|
|
74
86
|
* @returns Trimmed text
|
|
75
87
|
*/
|
|
76
88
|
export function normalizeText(value: string | null): string {
|
|
77
|
-
|
|
89
|
+
return (value ?? "").trim();
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
/**
|
|
81
93
|
* Extract plain text content from phrasing nodes (inline content).
|
|
82
94
|
* Strips all formatting and extracts text only.
|
|
83
|
-
*
|
|
95
|
+
*
|
|
84
96
|
* @param nodes - Array of phrasing content nodes
|
|
85
97
|
* @param definitions - Map of link reference definitions
|
|
86
98
|
* @returns Plain text string
|
|
87
99
|
*/
|
|
88
|
-
export function plainTextFromPhrasing(
|
|
89
|
-
|
|
100
|
+
export function plainTextFromPhrasing(
|
|
101
|
+
nodes: PhrasingContent[],
|
|
102
|
+
definitions: Map<string, Definition>,
|
|
103
|
+
): string {
|
|
104
|
+
return nodes
|
|
105
|
+
.map((node) => plainTextFromPhrasingNode(node, definitions))
|
|
106
|
+
.join("");
|
|
90
107
|
}
|
|
91
108
|
|
|
92
|
-
function plainTextFromPhrasingNode(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
function plainTextFromPhrasingNode(
|
|
110
|
+
node: PhrasingContent,
|
|
111
|
+
definitions: Map<string, Definition>,
|
|
112
|
+
): string {
|
|
113
|
+
switch (node.type) {
|
|
114
|
+
case "text":
|
|
115
|
+
return (node as Text).value;
|
|
116
|
+
case "strong":
|
|
117
|
+
case "emphasis":
|
|
118
|
+
return plainTextFromPhrasing((node as Strong).children, definitions);
|
|
119
|
+
case "inlineCode":
|
|
120
|
+
return (node as InlineCode).value;
|
|
121
|
+
case "link":
|
|
122
|
+
return plainTextFromPhrasing((node as Link).children, definitions);
|
|
123
|
+
case "linkReference": {
|
|
124
|
+
const lr = node as LinkReference;
|
|
125
|
+
const label = plainTextFromPhrasing(lr.children, definitions);
|
|
126
|
+
if (label.trim()) return label;
|
|
127
|
+
const def = definitions.get(lr.identifier.toLowerCase());
|
|
128
|
+
return def ? def.url : lr.label || lr.identifier;
|
|
129
|
+
}
|
|
130
|
+
case "break":
|
|
131
|
+
return "\n";
|
|
132
|
+
default:
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
115
135
|
}
|
|
116
136
|
|
|
117
137
|
/**
|
|
118
138
|
* Render a Typst-style tuple array.
|
|
119
139
|
* Ensures proper syntax for single-element arrays (requires trailing comma).
|
|
120
|
-
*
|
|
140
|
+
*
|
|
121
141
|
* @param items - Array items as strings
|
|
122
142
|
* @returns Formatted Typst array syntax
|
|
123
143
|
*/
|
|
124
144
|
export function renderTypstArray(items: string[]): string {
|
|
125
|
-
|
|
126
|
-
|
|
145
|
+
if (items.length === 1) return `(${items[0]},)`;
|
|
146
|
+
return `(${items.join(", ")})`;
|
|
127
147
|
}
|
|
128
148
|
|
|
129
149
|
/**
|
|
130
150
|
* Calculate the maximum consecutive backtick run in a string.
|
|
131
151
|
* Used to determine the fence length for code blocks.
|
|
132
|
-
*
|
|
152
|
+
*
|
|
133
153
|
* @param value - String to analyze
|
|
134
154
|
* @returns Maximum consecutive backtick count
|
|
135
155
|
*/
|
|
136
156
|
export function maxBacktickRun(value: string): number {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
let maxRun = 0;
|
|
158
|
+
let run = 0;
|
|
159
|
+
for (let i = 0; i < value.length; i++) {
|
|
160
|
+
if (value[i] === "`") {
|
|
161
|
+
run++;
|
|
162
|
+
if (run > maxRun) maxRun = run;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
run = 0;
|
|
166
|
+
}
|
|
167
|
+
return maxRun;
|
|
148
168
|
}
|
|
149
169
|
|
|
150
170
|
/**
|
|
151
171
|
* Coerce language string to valid ISO 639 language code.
|
|
152
|
-
*
|
|
172
|
+
*
|
|
153
173
|
* Validates and normalizes language codes using ISO 639-1 standard.
|
|
154
174
|
* Supports various input formats:
|
|
155
175
|
* - ISO 639-1 codes (e.g., 'en', 'zh', 'fr')
|
|
156
176
|
* - Locale codes (e.g., 'en-US', 'zh-CN') - extracts language part
|
|
157
177
|
* - Language names (e.g., 'English', 'Chinese') - looks up code
|
|
158
|
-
*
|
|
178
|
+
*
|
|
159
179
|
* @param value - Language string or code
|
|
160
180
|
* @returns Normalized ISO 639-1 language code or undefined if invalid
|
|
161
181
|
*/
|
|
162
182
|
export function coerceLanguage(value: string | undefined): string | undefined {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
183
|
+
if (!value) return undefined;
|
|
184
|
+
|
|
185
|
+
const v = value.trim();
|
|
186
|
+
if (!v) return undefined;
|
|
187
|
+
|
|
188
|
+
// Extract language code from locale format (e.g., 'en-US' -> 'en', 'zh-CN' -> 'zh')
|
|
189
|
+
const langPart = v.split(/[-_]/)[0].toLowerCase();
|
|
190
|
+
|
|
191
|
+
// Try to validate as ISO 639-1 code (2-letter)
|
|
192
|
+
const iso1Codes = langs.codes("1");
|
|
193
|
+
if (iso1Codes.includes(langPart)) {
|
|
194
|
+
return langPart;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Try to look up by language name (case-insensitive)
|
|
198
|
+
const allLangs = langs.all();
|
|
199
|
+
for (const lang of allLangs) {
|
|
200
|
+
if (lang.name && lang.name.toLowerCase() === v.toLowerCase() && lang["1"]) {
|
|
201
|
+
return lang["1"];
|
|
202
|
+
}
|
|
203
|
+
// Also check local name if available
|
|
204
|
+
if (
|
|
205
|
+
lang.local &&
|
|
206
|
+
lang.local.toLowerCase() === v.toLowerCase() &&
|
|
207
|
+
lang["1"]
|
|
208
|
+
) {
|
|
209
|
+
return lang["1"];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Try ISO 639-2 or 639-3 codes and convert to ISO 639-1
|
|
214
|
+
const langBy2 = langs.where("2", v.toLowerCase());
|
|
215
|
+
if (langBy2 && langBy2["1"]) {
|
|
216
|
+
return langBy2["1"];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const langBy2B = langs.where("2B", v.toLowerCase());
|
|
220
|
+
if (langBy2B && langBy2B["1"]) {
|
|
221
|
+
return langBy2B["1"];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const langBy3 = langs.where("3", v.toLowerCase());
|
|
225
|
+
if (langBy3 && langBy3["1"]) {
|
|
226
|
+
return langBy3["1"];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// If no valid code found, return undefined
|
|
230
|
+
return undefined;
|
|
207
231
|
}
|
|
208
232
|
|
|
209
233
|
/**
|
|
210
234
|
* Coerce region string to valid ISO 3166 country code.
|
|
211
|
-
*
|
|
235
|
+
*
|
|
212
236
|
* Validates and normalizes region/country codes using ISO 3166-1 alpha-2 standard.
|
|
213
237
|
* Supports various input formats:
|
|
214
238
|
* - ISO 3166-1 alpha-2 codes (e.g., 'US', 'CN', 'GB')
|
|
215
239
|
* - Country names (e.g., 'United States', 'China') - looks up code
|
|
216
|
-
*
|
|
240
|
+
*
|
|
217
241
|
* @param value - Region/country string or code
|
|
218
242
|
* @returns Normalized ISO 3166-1 alpha-2 country code or undefined if invalid
|
|
219
243
|
*/
|
|
220
244
|
export function coerceRegion(value: string | undefined): string | undefined {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
245
|
+
if (!value) return undefined;
|
|
246
|
+
|
|
247
|
+
const v = value.trim();
|
|
248
|
+
if (!v) return undefined;
|
|
249
|
+
|
|
250
|
+
// Check if it's already a valid ISO 3166-1 alpha-2 code
|
|
251
|
+
const upperValue = v.toUpperCase();
|
|
252
|
+
const validCodes = getCodes();
|
|
253
|
+
if (validCodes.includes(upperValue)) {
|
|
254
|
+
return upperValue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Try to look up by country name
|
|
258
|
+
const code = getCode(v);
|
|
259
|
+
if (code) {
|
|
260
|
+
return code.toUpperCase();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// If no valid code found, return undefined
|
|
264
|
+
return undefined;
|
|
241
265
|
}
|