@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 ADDED
@@ -0,0 +1,23 @@
1
+ # @zhachory1/mewrite-markdown-preview
2
+
3
+ Rendered markdown + LaTeX preview for [Me Write Code](https://github.com/Zhachory1/mewritecode) — terminal, browser, and PDF output.
4
+
5
+ Loaded as a `@zhachory1/mewrite-code` extension; not intended as a standalone library.
6
+
7
+ ## Install
8
+
9
+ Bundled with `@zhachory1/mewrite-code` by default. To load explicitly:
10
+
11
+ ```bash
12
+ caveman --extension @zhachory1/mewrite-markdown-preview "render this README"
13
+ ```
14
+
15
+ ## What it does
16
+
17
+ - Renders markdown to a paginated, styled terminal view.
18
+ - Exports the same rendered output to a single-file HTML page or PDF via headless Chrome.
19
+ - Handles LaTeX math via KaTeX.
20
+
21
+ ## License
22
+
23
+ MIT — see [LICENSE](../../LICENSE).
@@ -0,0 +1,543 @@
1
+ (() => {
2
+ function normalizePreviewAnnotationLabel(text) {
3
+ return String(text || "")
4
+ .replace(/\r\n/g, "\n")
5
+ .replace(/\s*\n\s*/g, " ")
6
+ .replace(/\s{2,}/g, " ")
7
+ .trim();
8
+ }
9
+
10
+ function advancePastBacktickSpan(source, startIndex) {
11
+ let fenceLength = 1;
12
+ while (source[startIndex + fenceLength] === "`") fenceLength += 1;
13
+
14
+ let index = startIndex + fenceLength;
15
+ while (index < source.length) {
16
+ const ch = source[index];
17
+ if (ch === "\\") {
18
+ index = Math.min(source.length, index + 2);
19
+ continue;
20
+ }
21
+ if (ch === "`") {
22
+ let runLength = 1;
23
+ while (source[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 source.length;
37
+ }
38
+
39
+ 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 = advancePastBacktickSpan(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: 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
+ 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 = advancePastBacktickSpan(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
+ 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
+ function escapeHtml(text) {
145
+ return String(text || "")
146
+ .replace(/&/g, "&amp;")
147
+ .replace(/</g, "&lt;")
148
+ .replace(/>/g, "&gt;")
149
+ .replace(/"/g, "&quot;")
150
+ .replace(/'/g, "&#39;");
151
+ }
152
+
153
+ function isWordChar(ch) {
154
+ return typeof ch === "string" && /[A-Za-z0-9]/.test(ch);
155
+ }
156
+
157
+ function readInlineMarkdownLinkAt(source, startIndex) {
158
+ const text = String(source || "");
159
+ if (text[startIndex] !== "[") return null;
160
+
161
+ let index = startIndex + 1;
162
+ let squareDepth = 0;
163
+ while (index < text.length) {
164
+ const ch = text[index];
165
+ if (ch === "\\") {
166
+ index = Math.min(text.length, index + 2);
167
+ continue;
168
+ }
169
+ if (ch === "`") {
170
+ index = advancePastBacktickSpan(text, index);
171
+ continue;
172
+ }
173
+ if (ch === "[") {
174
+ squareDepth += 1;
175
+ index += 1;
176
+ continue;
177
+ }
178
+ if (ch === "]") {
179
+ if (squareDepth === 0) break;
180
+ squareDepth -= 1;
181
+ index += 1;
182
+ continue;
183
+ }
184
+ if (ch === "\n") return null;
185
+ index += 1;
186
+ }
187
+
188
+ if (index >= text.length || text[index] !== "]" || text[index + 1] !== "(") return null;
189
+
190
+ index += 2;
191
+ let parenDepth = 0;
192
+ while (index < text.length) {
193
+ const ch = text[index];
194
+ if (ch === "\\") {
195
+ index = Math.min(text.length, index + 2);
196
+ continue;
197
+ }
198
+ if (ch === "`") {
199
+ index = advancePastBacktickSpan(text, index);
200
+ continue;
201
+ }
202
+ if (ch === "(") {
203
+ parenDepth += 1;
204
+ index += 1;
205
+ continue;
206
+ }
207
+ if (ch === ")") {
208
+ if (parenDepth === 0) {
209
+ return {
210
+ type: "literal",
211
+ raw: text.slice(startIndex, index + 1),
212
+ end: index + 1,
213
+ };
214
+ }
215
+ parenDepth -= 1;
216
+ index += 1;
217
+ continue;
218
+ }
219
+ if (ch === "\n") return null;
220
+ index += 1;
221
+ }
222
+
223
+ return null;
224
+ }
225
+
226
+ function readDelimitedPreviewTokenAt(source, startIndex, open, close, allowNewlines) {
227
+ const text = String(source || "");
228
+ if (text.slice(startIndex, startIndex + open.length) !== open) return null;
229
+
230
+ let index = startIndex + open.length;
231
+ while (index < text.length) {
232
+ const ch = text[index];
233
+ if (!allowNewlines && ch === "\n") return null;
234
+ if (ch === "\\") {
235
+ index = Math.min(text.length, index + 2);
236
+ continue;
237
+ }
238
+ if (text.slice(index, index + close.length) === close) {
239
+ return {
240
+ type: "math",
241
+ raw: text.slice(startIndex, index + close.length),
242
+ end: index + close.length,
243
+ };
244
+ }
245
+ index += 1;
246
+ }
247
+
248
+ return null;
249
+ }
250
+
251
+ function readInlineMathTokenAt(source, startIndex) {
252
+ const text = String(source || "");
253
+ if (text[startIndex] === "\\" && text[startIndex + 1] === "(") {
254
+ return readDelimitedPreviewTokenAt(text, startIndex, "\\(", "\\)", true);
255
+ }
256
+ if (text[startIndex] === "\\" && text[startIndex + 1] === "[") {
257
+ return readDelimitedPreviewTokenAt(text, startIndex, "\\[", "\\]", true);
258
+ }
259
+ if (text[startIndex] === "$" && text[startIndex + 1] === "$") {
260
+ return readDelimitedPreviewTokenAt(text, startIndex, "$$", "$$", true);
261
+ }
262
+ if (text[startIndex] === "$" && text[startIndex + 1] !== "$" && text[startIndex + 1] && !/\s/.test(text[startIndex + 1])) {
263
+ const token = readDelimitedPreviewTokenAt(text, startIndex, "$", "$", false);
264
+ if (token && token.raw.length > 2) return token;
265
+ }
266
+ return null;
267
+ }
268
+
269
+ function readBareUrlTokenAt(source, startIndex) {
270
+ const text = String(source || "").slice(startIndex);
271
+ const match = text.match(/^https?:\/\/[^\s<]+/i);
272
+ if (!match) return null;
273
+ return {
274
+ type: "literal",
275
+ raw: match[0],
276
+ end: startIndex + match[0].length,
277
+ };
278
+ }
279
+
280
+ function readAnnotationPreviewProtectedTokenAt(source, startIndex) {
281
+ const text = String(source || "");
282
+ if (startIndex < 0 || startIndex >= text.length) return null;
283
+
284
+ if (text[startIndex] === "`") {
285
+ const end = advancePastBacktickSpan(text, startIndex);
286
+ return {
287
+ type: "code",
288
+ raw: text.slice(startIndex, end),
289
+ end: end,
290
+ };
291
+ }
292
+
293
+ const linkToken = text[startIndex] === "["
294
+ ? readInlineMarkdownLinkAt(text, startIndex)
295
+ : null;
296
+ if (linkToken) return linkToken;
297
+
298
+ const mathToken = (text[startIndex] === "$" || text[startIndex] === "\\")
299
+ ? readInlineMathTokenAt(text, startIndex)
300
+ : null;
301
+ if (mathToken) return mathToken;
302
+
303
+ const urlToken = text[startIndex].toLowerCase() === "h"
304
+ ? readBareUrlTokenAt(text, startIndex)
305
+ : null;
306
+ if (urlToken) return urlToken;
307
+
308
+ return null;
309
+ }
310
+
311
+ function canOpenEmphasisDelimiter(source, startIndex, delimiter) {
312
+ const text = String(source || "");
313
+ if (text.slice(startIndex, startIndex + delimiter.length) !== delimiter) return false;
314
+ const prev = startIndex > 0 ? text[startIndex - 1] : "";
315
+ const next = text[startIndex + delimiter.length] || "";
316
+ if (!next || /\s/.test(next)) return false;
317
+ return !isWordChar(prev);
318
+ }
319
+
320
+ function canCloseEmphasisDelimiter(source, startIndex, delimiter) {
321
+ const text = String(source || "");
322
+ if (text.slice(startIndex, startIndex + delimiter.length) !== delimiter) return false;
323
+ const prev = startIndex > 0 ? text[startIndex - 1] : "";
324
+ const next = text[startIndex + delimiter.length] || "";
325
+ if (!prev || /\s/.test(prev)) return false;
326
+ return !isWordChar(next);
327
+ }
328
+
329
+ function readAnnotationEmphasisSpanAt(source, startIndex, delimiter, tagName) {
330
+ const text = String(source || "");
331
+ if (!canOpenEmphasisDelimiter(text, startIndex, delimiter)) return null;
332
+
333
+ let index = startIndex + delimiter.length;
334
+ while (index < text.length) {
335
+ if (text[index] === "\\") {
336
+ index = Math.min(text.length, index + 2);
337
+ continue;
338
+ }
339
+
340
+ const protectedToken = readAnnotationPreviewProtectedTokenAt(text, index);
341
+ if (protectedToken) {
342
+ index = protectedToken.end;
343
+ continue;
344
+ }
345
+
346
+ if (canCloseEmphasisDelimiter(text, index, delimiter)) {
347
+ const inner = text.slice(startIndex + delimiter.length, index);
348
+ return {
349
+ end: index + delimiter.length,
350
+ html: "<" + tagName + ">" + renderAnnotationPlainTextHtml(inner) + "</" + tagName + ">",
351
+ };
352
+ }
353
+
354
+ index += 1;
355
+ }
356
+
357
+ return null;
358
+ }
359
+
360
+ function renderAnnotationCodeSpanHtml(rawToken) {
361
+ const raw = String(rawToken || "");
362
+ if (!raw || raw[0] !== "`") return escapeHtml(raw);
363
+
364
+ let fenceLength = 1;
365
+ while (raw[fenceLength] === "`") fenceLength += 1;
366
+ const fence = "`".repeat(fenceLength);
367
+ if (raw.length < fenceLength * 2 || raw.slice(raw.length - fenceLength) !== fence) {
368
+ return escapeHtml(raw);
369
+ }
370
+
371
+ return "<code>" + escapeHtml(raw.slice(fenceLength, raw.length - fenceLength)) + "</code>";
372
+ }
373
+
374
+ function renderAnnotationPlainTextHtml(text) {
375
+ const source = String(text || "");
376
+ let out = "";
377
+ let index = 0;
378
+
379
+ while (index < source.length) {
380
+ const strongMatch = readAnnotationEmphasisSpanAt(source, index, "**", "strong")
381
+ || readAnnotationEmphasisSpanAt(source, index, "__", "strong");
382
+ if (strongMatch) {
383
+ out += strongMatch.html;
384
+ index = strongMatch.end;
385
+ continue;
386
+ }
387
+
388
+ const emphasisMatch = readAnnotationEmphasisSpanAt(source, index, "*", "em")
389
+ || readAnnotationEmphasisSpanAt(source, index, "_", "em");
390
+ if (emphasisMatch) {
391
+ out += emphasisMatch.html;
392
+ index = emphasisMatch.end;
393
+ continue;
394
+ }
395
+
396
+ out += escapeHtml(source[index]);
397
+ index += 1;
398
+ }
399
+
400
+ return out;
401
+ }
402
+
403
+ function renderPreviewAnnotationHtml(text) {
404
+ const source = normalizePreviewAnnotationLabel(text);
405
+ let out = "";
406
+ let plainStart = 0;
407
+ let index = 0;
408
+
409
+ while (index < source.length) {
410
+ const token = readAnnotationPreviewProtectedTokenAt(source, index);
411
+ if (!token) {
412
+ index += 1;
413
+ continue;
414
+ }
415
+
416
+ if (index > plainStart) {
417
+ out += renderAnnotationPlainTextHtml(source.slice(plainStart, index));
418
+ }
419
+
420
+ if (token.type === "code") {
421
+ out += renderAnnotationCodeSpanHtml(token.raw);
422
+ } else {
423
+ out += escapeHtml(token.raw);
424
+ }
425
+
426
+ index = token.end;
427
+ plainStart = index;
428
+ }
429
+
430
+ if (plainStart < source.length) {
431
+ out += renderAnnotationPlainTextHtml(source.slice(plainStart));
432
+ }
433
+
434
+ return out;
435
+ }
436
+
437
+ function transformMarkdownOutsideFences(text, plainTransformer) {
438
+ const source = String(text || "").replace(/\r\n/g, "\n");
439
+ if (!source) return source;
440
+
441
+ const transformPlain = typeof plainTransformer === "function"
442
+ ? plainTransformer
443
+ : function(segment) { return segment; };
444
+ const lines = source.split("\n");
445
+ const out = [];
446
+ let plainBuffer = [];
447
+ let inFence = false;
448
+ let fenceChar = null;
449
+ let fenceLength = 0;
450
+
451
+ function flushPlain() {
452
+ if (plainBuffer.length === 0) return;
453
+ const transformed = transformPlain(plainBuffer.join("\n"));
454
+ out.push(typeof transformed === "string" ? transformed : String(transformed ?? ""));
455
+ plainBuffer = [];
456
+ }
457
+
458
+ lines.forEach(function(line) {
459
+ const trimmed = line.trimStart();
460
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
461
+ if (fenceMatch) {
462
+ const marker = fenceMatch[1] || "";
463
+ const markerChar = marker.charAt(0);
464
+ const markerLength = marker.length;
465
+
466
+ if (!inFence) {
467
+ flushPlain();
468
+ inFence = true;
469
+ fenceChar = markerChar;
470
+ fenceLength = markerLength;
471
+ out.push(line);
472
+ return;
473
+ }
474
+
475
+ if (fenceChar === markerChar && markerLength >= fenceLength) {
476
+ inFence = false;
477
+ fenceChar = null;
478
+ fenceLength = 0;
479
+ }
480
+
481
+ out.push(line);
482
+ return;
483
+ }
484
+
485
+ if (inFence) {
486
+ out.push(line);
487
+ } else {
488
+ plainBuffer.push(line);
489
+ }
490
+ });
491
+
492
+ flushPlain();
493
+ return out.join("\n");
494
+ }
495
+
496
+ function hasAnnotationMarkers(text) {
497
+ let found = false;
498
+ transformMarkdownOutsideFences(text, function(segment) {
499
+ if (!found && collectInlineAnnotationMarkers(segment).length > 0) {
500
+ found = true;
501
+ }
502
+ return segment;
503
+ });
504
+ return found;
505
+ }
506
+
507
+ function stripAnnotationMarkers(text) {
508
+ return transformMarkdownOutsideFences(text, function(segment) {
509
+ return replaceInlineAnnotationMarkers(segment, function() { return ""; });
510
+ });
511
+ }
512
+
513
+ function prepareMarkdownForPandocPreview(markdown, placeholderPrefix) {
514
+ const placeholders = [];
515
+ const prefix = typeof placeholderPrefix === "string" && placeholderPrefix
516
+ ? placeholderPrefix
517
+ : "PIMDANNOT";
518
+ const prepared = transformMarkdownOutsideFences(markdown, function(segment) {
519
+ return replaceInlineAnnotationMarkers(segment, function(marker) {
520
+ const label = normalizePreviewAnnotationLabel(marker.body);
521
+ if (!label) return "";
522
+ const token = prefix + placeholders.length + "TOKEN";
523
+ placeholders.push({ token: token, text: label, title: "[an: " + label + "]" });
524
+ return token;
525
+ });
526
+ });
527
+ return { markdown: prepared, placeholders: placeholders };
528
+ }
529
+
530
+ const helpers = Object.freeze({
531
+ collectInlineAnnotationMarkers: collectInlineAnnotationMarkers,
532
+ hasAnnotationMarkers: hasAnnotationMarkers,
533
+ normalizePreviewAnnotationLabel: normalizePreviewAnnotationLabel,
534
+ prepareMarkdownForPandocPreview: prepareMarkdownForPandocPreview,
535
+ readInlineAnnotationMarkerAt: readInlineAnnotationMarkerAt,
536
+ renderPreviewAnnotationHtml: renderPreviewAnnotationHtml,
537
+ replaceInlineAnnotationMarkers: replaceInlineAnnotationMarkers,
538
+ stripAnnotationMarkers: stripAnnotationMarkers,
539
+ transformMarkdownOutsideFences: transformMarkdownOutsideFences,
540
+ });
541
+
542
+ globalThis.PiMarkdownPreviewAnnotationHelpers = helpers;
543
+ })();