markdown2typst 0.1.4 → 0.1.6

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/src/utils.ts CHANGED
@@ -3,239 +3,263 @@
3
3
  * @module utils
4
4
  */
5
5
 
6
- import langs from 'langs';
7
- import { getCode, getCodes } from 'country-list';
8
- import type { Definition, PhrasingContent, Text, InlineCode, Strong, Link, LinkReference } from 'mdast';
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
- try {
19
- return input.replace(/[\\#*_`\[\]\$<>@]/g, (c) => `\\${c}`);
20
- } catch (error) {
21
- // Fallback: return original string if escaping fails
22
- return input;
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
- try {
35
- return input.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
36
- } catch (error) {
37
- // Fallback: return original string if escaping fails
38
- return input;
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
- if (!indentLevel) return text;
52
- const indent = ' '.repeat(indentLevel);
53
- return text
54
- .split('\n')
55
- .map((line) => `${indent}${line}`)
56
- .join('\n');
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
- return typeof value === 'string' && value.length > 0;
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
- return (value ?? '').trim();
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(nodes: PhrasingContent[], definitions: Map<string, Definition>): string {
89
- return nodes.map((node) => plainTextFromPhrasingNode(node, definitions)).join('');
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(node: PhrasingContent, definitions: Map<string, Definition>): string {
93
- switch (node.type) {
94
- case 'text':
95
- return (node as Text).value;
96
- case 'strong':
97
- case 'emphasis':
98
- return plainTextFromPhrasing((node as Strong).children, definitions);
99
- case 'inlineCode':
100
- return (node as InlineCode).value;
101
- case 'link':
102
- return plainTextFromPhrasing((node as Link).children, definitions);
103
- case 'linkReference': {
104
- const lr = node as LinkReference;
105
- const label = plainTextFromPhrasing(lr.children, definitions);
106
- if (label.trim()) return label;
107
- const def = definitions.get(lr.identifier.toLowerCase());
108
- return def ? def.url : lr.label || lr.identifier;
109
- }
110
- case 'break':
111
- return '\n';
112
- default:
113
- return '';
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
- if (items.length === 1) return `(${items[0]},)`;
126
- return `(${items.join(', ')})`;
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
- let maxRun = 0;
138
- let run = 0;
139
- for (let i = 0; i < value.length; i++) {
140
- if (value[i] === '`') {
141
- run++;
142
- if (run > maxRun) maxRun = run;
143
- continue;
144
- }
145
- run = 0;
146
- }
147
- return maxRun;
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
- if (!value) return undefined;
164
-
165
- const v = value.trim();
166
- if (!v) return undefined;
167
-
168
- // Extract language code from locale format (e.g., 'en-US' -> 'en', 'zh-CN' -> 'zh')
169
- const langPart = v.split(/[-_]/)[0].toLowerCase();
170
-
171
- // Try to validate as ISO 639-1 code (2-letter)
172
- const iso1Codes = langs.codes('1');
173
- if (iso1Codes.includes(langPart)) {
174
- return langPart;
175
- }
176
-
177
- // Try to look up by language name (case-insensitive)
178
- const allLangs = langs.all();
179
- for (const lang of allLangs) {
180
- if (lang.name && lang.name.toLowerCase() === v.toLowerCase() && lang['1']) {
181
- return lang['1'];
182
- }
183
- // Also check local name if available
184
- if (lang.local && lang.local.toLowerCase() === v.toLowerCase() && lang['1']) {
185
- return lang['1'];
186
- }
187
- }
188
-
189
- // Try ISO 639-2 or 639-3 codes and convert to ISO 639-1
190
- const langBy2 = langs.where('2', v.toLowerCase());
191
- if (langBy2 && langBy2['1']) {
192
- return langBy2['1'];
193
- }
194
-
195
- const langBy2B = langs.where('2B', v.toLowerCase());
196
- if (langBy2B && langBy2B['1']) {
197
- return langBy2B['1'];
198
- }
199
-
200
- const langBy3 = langs.where('3', v.toLowerCase());
201
- if (langBy3 && langBy3['1']) {
202
- return langBy3['1'];
203
- }
204
-
205
- // If no valid code found, return undefined
206
- return undefined;
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
- if (!value) return undefined;
222
-
223
- const v = value.trim();
224
- if (!v) return undefined;
225
-
226
- // Check if it's already a valid ISO 3166-1 alpha-2 code
227
- const upperValue = v.toUpperCase();
228
- const validCodes = getCodes();
229
- if (validCodes.includes(upperValue)) {
230
- return upperValue;
231
- }
232
-
233
- // Try to look up by country name
234
- const code = getCode(v);
235
- if (code) {
236
- return code.toUpperCase();
237
- }
238
-
239
- // If no valid code found, return undefined
240
- return undefined;
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
  }