@zhachory1/mewrite-markdown-preview 0.9.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/README.md +23 -0
- package/client/annotation-helpers.js +543 -0
- package/index.ts +3240 -0
- package/package.json +55 -0
- package/shared/annotation-scanner.js +393 -0
- package/tsconfig.json +13 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zhachory1/mewrite-markdown-preview",
|
|
3
|
+
"version": "0.9.6",
|
|
4
|
+
"description": "Rendered markdown + LaTeX preview for mewrite-code, with terminal, browser, and PDF output",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"pi": {
|
|
7
|
+
"extensions": [
|
|
8
|
+
"./index.ts"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pi-package",
|
|
14
|
+
"mewrite-code",
|
|
15
|
+
"markdown",
|
|
16
|
+
"preview"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
"index.ts",
|
|
20
|
+
"client",
|
|
21
|
+
"shared",
|
|
22
|
+
"tsconfig.json",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"clean": "echo \"no build artifacts to clean\"",
|
|
27
|
+
"build": "echo \"no build step — ships .ts source as extension\""
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@zhachory1/mewrite-code": "*",
|
|
31
|
+
"@zhachory1/mewrite-tui": "*"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"puppeteer-core": "^24.22.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@zhachory1/mewrite-code": "*",
|
|
38
|
+
"@zhachory1/mewrite-tui": "*",
|
|
39
|
+
"@types/node": "^24.3.0",
|
|
40
|
+
"typescript": "^5.7.3"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/Zhachory1/mewritecode.git",
|
|
45
|
+
"directory": "packages/markdown-preview"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/Zhachory1/mewritecode#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/Zhachory1/mewritecode/issues"
|
|
50
|
+
},
|
|
51
|
+
"author": "Julius Brussee",
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
export function normalizeAnnotationText(text) {
|
|
2
|
+
return String(text || "")
|
|
3
|
+
.replace(/\r\n/g, "\n")
|
|
4
|
+
.replace(/\s*\n\s*/g, " ")
|
|
5
|
+
.replace(/\s{2,}/g, " ")
|
|
6
|
+
.trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function advancePastInlineBacktickSpan(source, startIndex) {
|
|
10
|
+
const text = String(source || "");
|
|
11
|
+
let fenceLength = 1;
|
|
12
|
+
while (text[startIndex + fenceLength] === "`") fenceLength += 1;
|
|
13
|
+
|
|
14
|
+
let index = startIndex + fenceLength;
|
|
15
|
+
while (index < text.length) {
|
|
16
|
+
const ch = text[index];
|
|
17
|
+
if (ch === "\\") {
|
|
18
|
+
index = Math.min(text.length, index + 2);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (ch === "`") {
|
|
22
|
+
let runLength = 1;
|
|
23
|
+
while (text[index + runLength] === "`") runLength += 1;
|
|
24
|
+
if (runLength === fenceLength) {
|
|
25
|
+
return index + runLength;
|
|
26
|
+
}
|
|
27
|
+
index += runLength;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (ch === "\n") {
|
|
31
|
+
return index + 1;
|
|
32
|
+
}
|
|
33
|
+
index += 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return text.length;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readInlineAnnotationMarkerAt(source, startIndex) {
|
|
40
|
+
const text = String(source || "");
|
|
41
|
+
if (startIndex < 0 || startIndex + 4 > text.length) return null;
|
|
42
|
+
if (text[startIndex] !== "[" || text.slice(startIndex, startIndex + 4).toLowerCase() !== "[an:") {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let index = startIndex + 4;
|
|
47
|
+
while (index < text.length && /\s/.test(text[index])) index += 1;
|
|
48
|
+
const bodyStart = index;
|
|
49
|
+
let squareDepth = 0;
|
|
50
|
+
|
|
51
|
+
while (index < text.length) {
|
|
52
|
+
const ch = text[index];
|
|
53
|
+
if (ch === "\\") {
|
|
54
|
+
index = Math.min(text.length, index + 2);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === "`") {
|
|
58
|
+
index = advancePastInlineBacktickSpan(text, index);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === "[") {
|
|
62
|
+
squareDepth += 1;
|
|
63
|
+
index += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (ch === "]") {
|
|
67
|
+
if (squareDepth === 0) {
|
|
68
|
+
const end = index + 1;
|
|
69
|
+
return {
|
|
70
|
+
start: startIndex,
|
|
71
|
+
end,
|
|
72
|
+
raw: text.slice(startIndex, end),
|
|
73
|
+
body: text.slice(bodyStart, index),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
squareDepth -= 1;
|
|
77
|
+
index += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
index += 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function collectInlineAnnotationMarkers(text) {
|
|
87
|
+
const source = String(text || "");
|
|
88
|
+
const markers = [];
|
|
89
|
+
let index = 0;
|
|
90
|
+
|
|
91
|
+
while (index < source.length) {
|
|
92
|
+
const ch = source[index];
|
|
93
|
+
if (ch === "\\") {
|
|
94
|
+
index = Math.min(source.length, index + 2);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (ch === "`") {
|
|
98
|
+
index = advancePastInlineBacktickSpan(source, index);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (ch === "[" && source.slice(index, index + 4).toLowerCase() === "[an:") {
|
|
102
|
+
const marker = readInlineAnnotationMarkerAt(source, index);
|
|
103
|
+
if (marker) {
|
|
104
|
+
markers.push(marker);
|
|
105
|
+
index = marker.end;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
index += 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return markers;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function replaceInlineAnnotationMarkers(text, annotationReplacer, textReplacer) {
|
|
116
|
+
const source = String(text || "");
|
|
117
|
+
const markers = collectInlineAnnotationMarkers(source);
|
|
118
|
+
const replaceAnnotation = typeof annotationReplacer === "function"
|
|
119
|
+
? annotationReplacer
|
|
120
|
+
: function(marker) { return marker.raw; };
|
|
121
|
+
const replaceText = typeof textReplacer === "function"
|
|
122
|
+
? textReplacer
|
|
123
|
+
: function(segment) { return segment; };
|
|
124
|
+
|
|
125
|
+
if (markers.length === 0) {
|
|
126
|
+
return replaceText(source);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let out = "";
|
|
130
|
+
let lastIndex = 0;
|
|
131
|
+
markers.forEach(function(marker) {
|
|
132
|
+
if (marker.start > lastIndex) {
|
|
133
|
+
out += String(replaceText(source.slice(lastIndex, marker.start)) ?? "");
|
|
134
|
+
}
|
|
135
|
+
out += String(replaceAnnotation(marker) ?? "");
|
|
136
|
+
lastIndex = marker.end;
|
|
137
|
+
});
|
|
138
|
+
if (lastIndex < source.length) {
|
|
139
|
+
out += String(replaceText(source.slice(lastIndex)) ?? "");
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function transformMarkdownOutsideFences(text, plainTransformer) {
|
|
145
|
+
const source = String(text || "").replace(/\r\n/g, "\n");
|
|
146
|
+
if (!source) return source;
|
|
147
|
+
|
|
148
|
+
const transformPlain = typeof plainTransformer === "function"
|
|
149
|
+
? plainTransformer
|
|
150
|
+
: function(segment) { return segment; };
|
|
151
|
+
const lines = source.split("\n");
|
|
152
|
+
const out = [];
|
|
153
|
+
let plainBuffer = [];
|
|
154
|
+
let inFence = false;
|
|
155
|
+
let fenceChar = null;
|
|
156
|
+
let fenceLength = 0;
|
|
157
|
+
|
|
158
|
+
function flushPlain() {
|
|
159
|
+
if (plainBuffer.length === 0) return;
|
|
160
|
+
const transformed = transformPlain(plainBuffer.join("\n"));
|
|
161
|
+
out.push(typeof transformed === "string" ? transformed : String(transformed ?? ""));
|
|
162
|
+
plainBuffer = [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
lines.forEach(function(line) {
|
|
166
|
+
const trimmed = line.trimStart();
|
|
167
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
168
|
+
if (fenceMatch) {
|
|
169
|
+
const marker = fenceMatch[1] || "";
|
|
170
|
+
const markerChar = marker.charAt(0);
|
|
171
|
+
const markerLength = marker.length;
|
|
172
|
+
|
|
173
|
+
if (!inFence) {
|
|
174
|
+
flushPlain();
|
|
175
|
+
inFence = true;
|
|
176
|
+
fenceChar = markerChar;
|
|
177
|
+
fenceLength = markerLength;
|
|
178
|
+
out.push(line);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (fenceChar === markerChar && markerLength >= fenceLength) {
|
|
183
|
+
inFence = false;
|
|
184
|
+
fenceChar = null;
|
|
185
|
+
fenceLength = 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
out.push(line);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (inFence) {
|
|
193
|
+
out.push(line);
|
|
194
|
+
} else {
|
|
195
|
+
plainBuffer.push(line);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
flushPlain();
|
|
200
|
+
return out.join("\n");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function hasMarkdownAnnotationMarkers(text) {
|
|
204
|
+
let found = false;
|
|
205
|
+
transformMarkdownOutsideFences(text, function(segment) {
|
|
206
|
+
if (!found && collectInlineAnnotationMarkers(segment).length > 0) {
|
|
207
|
+
found = true;
|
|
208
|
+
}
|
|
209
|
+
return segment;
|
|
210
|
+
});
|
|
211
|
+
return found;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function stripAnnotationMarkers(text) {
|
|
215
|
+
return transformMarkdownOutsideFences(text, function(segment) {
|
|
216
|
+
return replaceInlineAnnotationMarkers(segment, function() { return ""; });
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function prepareMarkdownForPandocPreview(markdown, placeholderPrefix) {
|
|
221
|
+
const placeholders = [];
|
|
222
|
+
const prefix = typeof placeholderPrefix === "string" && placeholderPrefix
|
|
223
|
+
? placeholderPrefix
|
|
224
|
+
: "PIMDANNOT";
|
|
225
|
+
const prepared = transformMarkdownOutsideFences(markdown, function(segment) {
|
|
226
|
+
return replaceInlineAnnotationMarkers(segment, function(marker) {
|
|
227
|
+
const label = normalizeAnnotationText(marker.body);
|
|
228
|
+
if (!label) return "";
|
|
229
|
+
const token = prefix + placeholders.length + "TOKEN";
|
|
230
|
+
placeholders.push({ token, text: label, title: "[an: " + label + "]" });
|
|
231
|
+
return token;
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
return { markdown: prepared, placeholders };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function isAnnotationWordChar(ch) {
|
|
238
|
+
return typeof ch === "string" && /[A-Za-z0-9]/.test(ch);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function readInlineMarkdownLinkAt(source, startIndex) {
|
|
242
|
+
const text = String(source || "");
|
|
243
|
+
if (text[startIndex] !== "[") return null;
|
|
244
|
+
|
|
245
|
+
let index = startIndex + 1;
|
|
246
|
+
let squareDepth = 0;
|
|
247
|
+
while (index < text.length) {
|
|
248
|
+
const ch = text[index];
|
|
249
|
+
if (ch === "\\") {
|
|
250
|
+
index = Math.min(text.length, index + 2);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (ch === "`") {
|
|
254
|
+
index = advancePastInlineBacktickSpan(text, index);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (ch === "[") {
|
|
258
|
+
squareDepth += 1;
|
|
259
|
+
index += 1;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (ch === "]") {
|
|
263
|
+
if (squareDepth === 0) break;
|
|
264
|
+
squareDepth -= 1;
|
|
265
|
+
index += 1;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (ch === "\n") return null;
|
|
269
|
+
index += 1;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (index >= text.length || text[index] !== "]" || text[index + 1] !== "(") return null;
|
|
273
|
+
|
|
274
|
+
index += 2;
|
|
275
|
+
let parenDepth = 0;
|
|
276
|
+
while (index < text.length) {
|
|
277
|
+
const ch = text[index];
|
|
278
|
+
if (ch === "\\") {
|
|
279
|
+
index = Math.min(text.length, index + 2);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (ch === "`") {
|
|
283
|
+
index = advancePastInlineBacktickSpan(text, index);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (ch === "(") {
|
|
287
|
+
parenDepth += 1;
|
|
288
|
+
index += 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (ch === ")") {
|
|
292
|
+
if (parenDepth === 0) {
|
|
293
|
+
return {
|
|
294
|
+
type: "literal",
|
|
295
|
+
raw: text.slice(startIndex, index + 1),
|
|
296
|
+
end: index + 1,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
parenDepth -= 1;
|
|
300
|
+
index += 1;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (ch === "\n") return null;
|
|
304
|
+
index += 1;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function readAnnotationDelimitedTokenAt(source, startIndex, open, close, allowNewlines) {
|
|
311
|
+
const text = String(source || "");
|
|
312
|
+
if (text.slice(startIndex, startIndex + open.length) !== open) return null;
|
|
313
|
+
|
|
314
|
+
let index = startIndex + open.length;
|
|
315
|
+
while (index < text.length) {
|
|
316
|
+
const ch = text[index];
|
|
317
|
+
if (!allowNewlines && ch === "\n") return null;
|
|
318
|
+
if (ch === "\\") {
|
|
319
|
+
index = Math.min(text.length, index + 2);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (text.slice(index, index + close.length) === close) {
|
|
323
|
+
return {
|
|
324
|
+
type: "math",
|
|
325
|
+
raw: text.slice(startIndex, index + close.length),
|
|
326
|
+
end: index + close.length,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
index += 1;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function readInlineMathTokenAt(source, startIndex) {
|
|
336
|
+
const text = String(source || "");
|
|
337
|
+
if (text[startIndex] === "\\" && text[startIndex + 1] === "(") {
|
|
338
|
+
return readAnnotationDelimitedTokenAt(text, startIndex, "\\(", "\\)", true);
|
|
339
|
+
}
|
|
340
|
+
if (text[startIndex] === "\\" && text[startIndex + 1] === "[") {
|
|
341
|
+
return readAnnotationDelimitedTokenAt(text, startIndex, "\\[", "\\]", true);
|
|
342
|
+
}
|
|
343
|
+
if (text[startIndex] === "$" && text[startIndex + 1] === "$") {
|
|
344
|
+
return readAnnotationDelimitedTokenAt(text, startIndex, "$$", "$$", true);
|
|
345
|
+
}
|
|
346
|
+
if (text[startIndex] === "$" && text[startIndex + 1] !== "$" && text[startIndex + 1] && !/\s/.test(text[startIndex + 1])) {
|
|
347
|
+
const token = readAnnotationDelimitedTokenAt(text, startIndex, "$", "$", false);
|
|
348
|
+
if (token && token.raw.length > 2) return token;
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function readBareUrlTokenAt(source, startIndex) {
|
|
354
|
+
const text = String(source || "").slice(startIndex);
|
|
355
|
+
const match = text.match(/^https?:\/\/[^\s<]+/i);
|
|
356
|
+
if (!match) return null;
|
|
357
|
+
return {
|
|
358
|
+
type: "literal",
|
|
359
|
+
raw: match[0],
|
|
360
|
+
end: startIndex + match[0].length,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function readAnnotationProtectedTokenAt(source, startIndex) {
|
|
365
|
+
const text = String(source || "");
|
|
366
|
+
if (startIndex < 0 || startIndex >= text.length) return null;
|
|
367
|
+
|
|
368
|
+
if (text[startIndex] === "`") {
|
|
369
|
+
const end = advancePastInlineBacktickSpan(text, startIndex);
|
|
370
|
+
return {
|
|
371
|
+
type: "code",
|
|
372
|
+
raw: text.slice(startIndex, end),
|
|
373
|
+
end,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const linkToken = text[startIndex] === "["
|
|
378
|
+
? readInlineMarkdownLinkAt(text, startIndex)
|
|
379
|
+
: null;
|
|
380
|
+
if (linkToken) return linkToken;
|
|
381
|
+
|
|
382
|
+
const mathToken = (text[startIndex] === "$" || text[startIndex] === "\\")
|
|
383
|
+
? readInlineMathTokenAt(text, startIndex)
|
|
384
|
+
: null;
|
|
385
|
+
if (mathToken) return mathToken;
|
|
386
|
+
|
|
387
|
+
const urlToken = text[startIndex].toLowerCase() === "h"
|
|
388
|
+
? readBareUrlTokenAt(text, startIndex)
|
|
389
|
+
: null;
|
|
390
|
+
if (urlToken) return urlToken;
|
|
391
|
+
|
|
392
|
+
return null;
|
|
393
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"types": ["node"]
|
|
11
|
+
},
|
|
12
|
+
"include": ["index.ts", "shared/**/*.ts"]
|
|
13
|
+
}
|