overtype 1.2.3 → 1.2.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 +36 -15
- package/dist/overtype.cjs +253 -57
- package/dist/overtype.cjs.map +2 -2
- package/dist/overtype.d.ts +169 -0
- package/dist/overtype.esm.js +253 -57
- package/dist/overtype.esm.js.map +2 -2
- package/dist/overtype.js +258 -57
- package/dist/overtype.js.map +2 -2
- package/dist/overtype.min.js +58 -42
- package/package.json +5 -2
- package/src/link-tooltip.js +16 -16
- package/src/overtype.d.ts +23 -1
- package/src/overtype.js +22 -12
- package/src/parser.js +272 -100
- package/src/styles.js +16 -8
- package/src/toolbar.js +63 -2
package/src/parser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MarkdownParser - Parses markdown into HTML while preserving character alignment
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Key principles:
|
|
5
5
|
* - Every character must occupy the exact same position as in the textarea
|
|
6
6
|
* - No font-size changes, no padding/margin on inline elements
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
export class MarkdownParser {
|
|
10
10
|
// Track link index for anchor naming
|
|
11
11
|
static linkIndex = 0;
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
/**
|
|
14
14
|
* Reset link index (call before parsing a new document)
|
|
15
15
|
*/
|
|
16
16
|
static resetLinkIndex() {
|
|
17
17
|
this.linkIndex = 0;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
/**
|
|
21
21
|
* Escape HTML special characters
|
|
22
22
|
* @param {string} text - Raw text to escape
|
|
@@ -134,8 +134,27 @@ export class MarkdownParser {
|
|
|
134
134
|
* @returns {string} HTML with italic styling
|
|
135
135
|
*/
|
|
136
136
|
static parseItalic(html) {
|
|
137
|
+
// Single asterisk - must not be adjacent to other asterisks
|
|
137
138
|
html = html.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em><span class="syntax-marker">*</span>$1<span class="syntax-marker">*</span></em>');
|
|
138
|
-
|
|
139
|
+
|
|
140
|
+
// Single underscore - must be at word boundaries to avoid matching inside words
|
|
141
|
+
// This prevents matching underscores in the middle of words like "bold_with_underscore"
|
|
142
|
+
html = html.replace(/(?<=^|\s)_(?!_)(.+?)(?<!_)_(?!_)(?=\s|$)/g, '<em><span class="syntax-marker">_</span>$1<span class="syntax-marker">_</span></em>');
|
|
143
|
+
|
|
144
|
+
return html;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Parse strikethrough text
|
|
149
|
+
* Supports both single (~) and double (~~) tildes, but rejects 3+ tildes
|
|
150
|
+
* @param {string} html - HTML with potential strikethrough markdown
|
|
151
|
+
* @returns {string} HTML with strikethrough styling
|
|
152
|
+
*/
|
|
153
|
+
static parseStrikethrough(html) {
|
|
154
|
+
// Double tilde strikethrough: ~~text~~ (but not if part of 3+ tildes)
|
|
155
|
+
html = html.replace(/(?<!~)~~(?!~)(.+?)(?<!~)~~(?!~)/g, '<del><span class="syntax-marker">~~</span>$1<span class="syntax-marker">~~</span></del>');
|
|
156
|
+
// Single tilde strikethrough: ~text~ (but not if part of 2+ tildes on either side)
|
|
157
|
+
html = html.replace(/(?<!~)~(?!~)(.+?)(?<!~)~(?!~)/g, '<del><span class="syntax-marker">~</span>$1<span class="syntax-marker">~</span></del>');
|
|
139
158
|
return html;
|
|
140
159
|
}
|
|
141
160
|
|
|
@@ -165,7 +184,7 @@ export class MarkdownParser {
|
|
|
165
184
|
// Trim whitespace and convert to lowercase for protocol check
|
|
166
185
|
const trimmed = url.trim();
|
|
167
186
|
const lower = trimmed.toLowerCase();
|
|
168
|
-
|
|
187
|
+
|
|
169
188
|
// Allow safe protocols
|
|
170
189
|
const safeProtocols = [
|
|
171
190
|
'http://',
|
|
@@ -174,22 +193,22 @@ export class MarkdownParser {
|
|
|
174
193
|
'ftp://',
|
|
175
194
|
'ftps://'
|
|
176
195
|
];
|
|
177
|
-
|
|
196
|
+
|
|
178
197
|
// Check if URL starts with a safe protocol
|
|
179
198
|
const hasSafeProtocol = safeProtocols.some(protocol => lower.startsWith(protocol));
|
|
180
|
-
|
|
199
|
+
|
|
181
200
|
// Allow relative URLs (starting with / or # or no protocol)
|
|
182
|
-
const isRelative = trimmed.startsWith('/') ||
|
|
183
|
-
trimmed.startsWith('#') ||
|
|
201
|
+
const isRelative = trimmed.startsWith('/') ||
|
|
202
|
+
trimmed.startsWith('#') ||
|
|
184
203
|
trimmed.startsWith('?') ||
|
|
185
204
|
trimmed.startsWith('.') ||
|
|
186
205
|
(!trimmed.includes(':') && !trimmed.includes('//'));
|
|
187
|
-
|
|
206
|
+
|
|
188
207
|
// If safe protocol or relative URL, return as-is
|
|
189
208
|
if (hasSafeProtocol || isRelative) {
|
|
190
209
|
return url;
|
|
191
210
|
}
|
|
192
|
-
|
|
211
|
+
|
|
193
212
|
// Block dangerous protocols (javascript:, data:, vbscript:, etc.)
|
|
194
213
|
return '#';
|
|
195
214
|
}
|
|
@@ -210,49 +229,158 @@ export class MarkdownParser {
|
|
|
210
229
|
}
|
|
211
230
|
|
|
212
231
|
/**
|
|
213
|
-
*
|
|
214
|
-
* @param {string} text - Text with potential
|
|
215
|
-
* @returns {
|
|
232
|
+
* Identify and protect sanctuaries (code and links) before parsing
|
|
233
|
+
* @param {string} text - Text with potential markdown
|
|
234
|
+
* @returns {Object} Object with protected text and sanctuary map
|
|
216
235
|
*/
|
|
217
|
-
static
|
|
218
|
-
let html = text;
|
|
219
|
-
// Order matters: parse code first
|
|
220
|
-
html = this.parseInlineCode(html);
|
|
221
|
-
|
|
222
|
-
// Use placeholders to protect inline code while preserving formatting spans
|
|
223
|
-
// We use Unicode Private Use Area (U+E000-U+F8FF) as placeholders because:
|
|
224
|
-
// 1. These characters are reserved for application-specific use
|
|
225
|
-
// 2. They'll never appear in user text
|
|
226
|
-
// 3. They maintain single-character width (important for alignment)
|
|
227
|
-
// 4. They're invisible if accidentally rendered
|
|
236
|
+
static identifyAndProtectSanctuaries(text) {
|
|
228
237
|
const sanctuaries = new Map();
|
|
238
|
+
let sanctuaryCounter = 0;
|
|
239
|
+
let protectedText = text;
|
|
240
|
+
|
|
241
|
+
// Create a map to track protected regions (URLs should not be processed)
|
|
242
|
+
const protectedRegions = [];
|
|
243
|
+
|
|
244
|
+
// First, find all links and mark their URL regions as protected
|
|
245
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
246
|
+
let linkMatch;
|
|
247
|
+
while ((linkMatch = linkRegex.exec(text)) !== null) {
|
|
248
|
+
// Calculate the exact position of the URL part
|
|
249
|
+
// linkMatch.index is the start of the match
|
|
250
|
+
// We need to find where "](" starts, then add 2 to get URL start
|
|
251
|
+
const bracketPos = linkMatch.index + linkMatch[0].indexOf('](');
|
|
252
|
+
const urlStart = bracketPos + 2;
|
|
253
|
+
const urlEnd = urlStart + linkMatch[2].length;
|
|
254
|
+
protectedRegions.push({ start: urlStart, end: urlEnd });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Now protect inline code, but skip if it's inside a protected region (URL)
|
|
258
|
+
const codeRegex = /(?<!`)(`+)(?!`)((?:(?!\1).)+?)(\1)(?!`)/g;
|
|
259
|
+
let codeMatch;
|
|
260
|
+
const codeMatches = [];
|
|
261
|
+
|
|
262
|
+
while ((codeMatch = codeRegex.exec(text)) !== null) {
|
|
263
|
+
const codeStart = codeMatch.index;
|
|
264
|
+
const codeEnd = codeMatch.index + codeMatch[0].length;
|
|
265
|
+
|
|
266
|
+
// Check if this code is inside a protected URL region
|
|
267
|
+
const inProtectedRegion = protectedRegions.some(region =>
|
|
268
|
+
codeStart >= region.start && codeEnd <= region.end
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (!inProtectedRegion) {
|
|
272
|
+
codeMatches.push({
|
|
273
|
+
match: codeMatch[0],
|
|
274
|
+
index: codeMatch.index,
|
|
275
|
+
openTicks: codeMatch[1],
|
|
276
|
+
content: codeMatch[2],
|
|
277
|
+
closeTicks: codeMatch[3]
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
229
281
|
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
282
|
+
// Replace code matches from end to start to preserve indices
|
|
283
|
+
codeMatches.sort((a, b) => b.index - a.index);
|
|
284
|
+
codeMatches.forEach(codeInfo => {
|
|
285
|
+
const placeholder = `\uE000${sanctuaryCounter++}\uE001`;
|
|
286
|
+
sanctuaries.set(placeholder, {
|
|
287
|
+
type: 'code',
|
|
288
|
+
original: codeInfo.match,
|
|
289
|
+
openTicks: codeInfo.openTicks,
|
|
290
|
+
content: codeInfo.content,
|
|
291
|
+
closeTicks: codeInfo.closeTicks
|
|
292
|
+
});
|
|
293
|
+
protectedText = protectedText.substring(0, codeInfo.index) +
|
|
294
|
+
placeholder +
|
|
295
|
+
protectedText.substring(codeInfo.index + codeInfo.match.length);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Then protect links - they can contain sanctuary placeholders for code but not raw code
|
|
299
|
+
protectedText = protectedText.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => {
|
|
300
|
+
const placeholder = `\uE000${sanctuaryCounter++}\uE001`;
|
|
301
|
+
sanctuaries.set(placeholder, {
|
|
302
|
+
type: 'link',
|
|
303
|
+
original: match,
|
|
304
|
+
linkText,
|
|
305
|
+
url
|
|
306
|
+
});
|
|
234
307
|
return placeholder;
|
|
235
308
|
});
|
|
236
309
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
310
|
+
return { protectedText, sanctuaries };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Restore and transform sanctuaries back to HTML
|
|
315
|
+
* @param {string} html - HTML with sanctuary placeholders
|
|
316
|
+
* @param {Map} sanctuaries - Map of sanctuaries to restore
|
|
317
|
+
* @returns {string} HTML with sanctuaries restored and transformed
|
|
318
|
+
*/
|
|
319
|
+
static restoreAndTransformSanctuaries(html, sanctuaries) {
|
|
320
|
+
// Sort sanctuary placeholders by position to restore in order
|
|
321
|
+
const placeholders = Array.from(sanctuaries.keys()).sort((a, b) => {
|
|
322
|
+
const indexA = html.indexOf(a);
|
|
323
|
+
const indexB = html.indexOf(b);
|
|
324
|
+
return indexA - indexB;
|
|
325
|
+
});
|
|
240
326
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
327
|
+
placeholders.forEach(placeholder => {
|
|
328
|
+
const sanctuary = sanctuaries.get(placeholder);
|
|
329
|
+
let replacement;
|
|
330
|
+
|
|
331
|
+
if (sanctuary.type === 'code') {
|
|
332
|
+
// Transform code sanctuary to HTML
|
|
333
|
+
replacement = `<code><span class="syntax-marker">${sanctuary.openTicks}</span>${this.escapeHtml(sanctuary.content)}<span class="syntax-marker">${sanctuary.closeTicks}</span></code>`;
|
|
334
|
+
} else if (sanctuary.type === 'link') {
|
|
335
|
+
// For links, we need to process the link text for markdown
|
|
336
|
+
let processedLinkText = sanctuary.linkText;
|
|
337
|
+
|
|
338
|
+
// First restore any sanctuary placeholders that were already in the link text
|
|
339
|
+
// (e.g., inline code that was protected before the link)
|
|
340
|
+
sanctuaries.forEach((innerSanctuary, innerPlaceholder) => {
|
|
341
|
+
if (processedLinkText.includes(innerPlaceholder)) {
|
|
342
|
+
if (innerSanctuary.type === 'code') {
|
|
343
|
+
const codeHtml = `<code><span class="syntax-marker">${innerSanctuary.openTicks}</span>${this.escapeHtml(innerSanctuary.content)}<span class="syntax-marker">${innerSanctuary.closeTicks}</span></code>`;
|
|
344
|
+
processedLinkText = processedLinkText.replace(innerPlaceholder, codeHtml);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Now parse other markdown in the link text (bold, italic, etc)
|
|
350
|
+
processedLinkText = this.parseStrikethrough(processedLinkText);
|
|
351
|
+
processedLinkText = this.parseBold(processedLinkText);
|
|
352
|
+
processedLinkText = this.parseItalic(processedLinkText);
|
|
353
|
+
|
|
354
|
+
// Transform link sanctuary to HTML
|
|
355
|
+
// URL should NOT be processed for markdown - use it as-is
|
|
356
|
+
const anchorName = `--link-${this.linkIndex++}`;
|
|
357
|
+
const safeUrl = this.sanitizeUrl(sanctuary.url);
|
|
358
|
+
replacement = `<a href="${safeUrl}" style="anchor-name: ${anchorName}"><span class="syntax-marker">[</span>${processedLinkText}<span class="syntax-marker url-part">](${this.escapeHtml(sanctuary.url)})</span></a>`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
html = html.replace(placeholder, replacement);
|
|
246
362
|
});
|
|
247
363
|
|
|
248
|
-
|
|
364
|
+
return html;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Parse all inline elements in correct order
|
|
369
|
+
* @param {string} text - Text with potential inline markdown
|
|
370
|
+
* @returns {string} HTML with all inline styling
|
|
371
|
+
*/
|
|
372
|
+
static parseInlineElements(text) {
|
|
373
|
+
// Step 1: Identify and protect sanctuaries (code and links)
|
|
374
|
+
const { protectedText, sanctuaries } = this.identifyAndProtectSanctuaries(text);
|
|
375
|
+
|
|
376
|
+
// Step 2: Parse other inline elements on protected text
|
|
377
|
+
let html = protectedText;
|
|
378
|
+
html = this.parseStrikethrough(html);
|
|
249
379
|
html = this.parseBold(html);
|
|
250
380
|
html = this.parseItalic(html);
|
|
251
381
|
|
|
252
|
-
// Restore
|
|
253
|
-
|
|
254
|
-
html = html.replace(placeholder, content);
|
|
255
|
-
});
|
|
382
|
+
// Step 3: Restore and transform sanctuaries
|
|
383
|
+
html = this.restoreAndTransformSanctuaries(html, sanctuaries);
|
|
256
384
|
|
|
257
385
|
return html;
|
|
258
386
|
}
|
|
@@ -264,33 +392,33 @@ export class MarkdownParser {
|
|
|
264
392
|
*/
|
|
265
393
|
static parseLine(line) {
|
|
266
394
|
let html = this.escapeHtml(line);
|
|
267
|
-
|
|
395
|
+
|
|
268
396
|
// Preserve indentation
|
|
269
397
|
html = this.preserveIndentation(html, line);
|
|
270
|
-
|
|
398
|
+
|
|
271
399
|
// Check for block elements first
|
|
272
400
|
const horizontalRule = this.parseHorizontalRule(html);
|
|
273
401
|
if (horizontalRule) return horizontalRule;
|
|
274
|
-
|
|
402
|
+
|
|
275
403
|
const codeBlock = this.parseCodeBlock(html);
|
|
276
404
|
if (codeBlock) return codeBlock;
|
|
277
|
-
|
|
405
|
+
|
|
278
406
|
// Parse block elements
|
|
279
407
|
html = this.parseHeader(html);
|
|
280
408
|
html = this.parseBlockquote(html);
|
|
281
409
|
html = this.parseBulletList(html);
|
|
282
410
|
html = this.parseNumberedList(html);
|
|
283
|
-
|
|
411
|
+
|
|
284
412
|
// Parse inline elements
|
|
285
413
|
html = this.parseInlineElements(html);
|
|
286
|
-
|
|
414
|
+
|
|
287
415
|
// Wrap in div to maintain line structure
|
|
288
416
|
if (html.trim() === '') {
|
|
289
417
|
// Intentionally use for empty lines to maintain vertical spacing
|
|
290
418
|
// This causes a 0->1 character count difference but preserves visual alignment
|
|
291
419
|
return '<div> </div>';
|
|
292
420
|
}
|
|
293
|
-
|
|
421
|
+
|
|
294
422
|
return `<div>${html}</div>`;
|
|
295
423
|
}
|
|
296
424
|
|
|
@@ -304,17 +432,17 @@ export class MarkdownParser {
|
|
|
304
432
|
static parse(text, activeLine = -1, showActiveLineRaw = false) {
|
|
305
433
|
// Reset link counter for each parse
|
|
306
434
|
this.resetLinkIndex();
|
|
307
|
-
|
|
435
|
+
|
|
308
436
|
const lines = text.split('\n');
|
|
309
437
|
let inCodeBlock = false;
|
|
310
|
-
|
|
438
|
+
|
|
311
439
|
const parsedLines = lines.map((line, index) => {
|
|
312
440
|
// Show raw markdown on active line if requested
|
|
313
441
|
if (showActiveLineRaw && index === activeLine) {
|
|
314
442
|
const content = this.escapeHtml(line) || ' ';
|
|
315
443
|
return `<div class="raw-line">${content}</div>`;
|
|
316
444
|
}
|
|
317
|
-
|
|
445
|
+
|
|
318
446
|
// Check if this line is a code fence
|
|
319
447
|
const codeFenceRegex = /^```[^`]*$/;
|
|
320
448
|
if (codeFenceRegex.test(line)) {
|
|
@@ -322,21 +450,21 @@ export class MarkdownParser {
|
|
|
322
450
|
// Parse fence markers normally to get styled output
|
|
323
451
|
return this.parseLine(line);
|
|
324
452
|
}
|
|
325
|
-
|
|
453
|
+
|
|
326
454
|
// If we're inside a code block, don't parse as markdown
|
|
327
455
|
if (inCodeBlock) {
|
|
328
456
|
const escaped = this.escapeHtml(line);
|
|
329
457
|
const indented = this.preserveIndentation(escaped, line);
|
|
330
458
|
return `<div>${indented || ' '}</div>`;
|
|
331
459
|
}
|
|
332
|
-
|
|
460
|
+
|
|
333
461
|
// Otherwise, parse the markdown normally
|
|
334
462
|
return this.parseLine(line);
|
|
335
463
|
});
|
|
336
|
-
|
|
464
|
+
|
|
337
465
|
// Join without newlines to prevent extra spacing
|
|
338
466
|
const html = parsedLines.join('');
|
|
339
|
-
|
|
467
|
+
|
|
340
468
|
// Apply post-processing for list consolidation
|
|
341
469
|
return this.postProcessHTML(html);
|
|
342
470
|
}
|
|
@@ -352,25 +480,25 @@ export class MarkdownParser {
|
|
|
352
480
|
// In Node.js environment - do manual post-processing
|
|
353
481
|
return this.postProcessHTMLManual(html);
|
|
354
482
|
}
|
|
355
|
-
|
|
483
|
+
|
|
356
484
|
// Parse HTML string into DOM
|
|
357
485
|
const container = document.createElement('div');
|
|
358
486
|
container.innerHTML = html;
|
|
359
|
-
|
|
487
|
+
|
|
360
488
|
let currentList = null;
|
|
361
489
|
let listType = null;
|
|
362
490
|
let currentCodeBlock = null;
|
|
363
491
|
let inCodeBlock = false;
|
|
364
|
-
|
|
492
|
+
|
|
365
493
|
// Process all direct children - need to be careful with live NodeList
|
|
366
494
|
const children = Array.from(container.children);
|
|
367
|
-
|
|
495
|
+
|
|
368
496
|
for (let i = 0; i < children.length; i++) {
|
|
369
497
|
const child = children[i];
|
|
370
|
-
|
|
498
|
+
|
|
371
499
|
// Skip if child was already processed/removed
|
|
372
500
|
if (!child.parentNode) continue;
|
|
373
|
-
|
|
501
|
+
|
|
374
502
|
// Check for code fence start/end
|
|
375
503
|
const codeFence = child.querySelector('.code-fence');
|
|
376
504
|
if (codeFence) {
|
|
@@ -379,22 +507,22 @@ export class MarkdownParser {
|
|
|
379
507
|
if (!inCodeBlock) {
|
|
380
508
|
// Start of code block - keep fence visible, then add pre/code
|
|
381
509
|
inCodeBlock = true;
|
|
382
|
-
|
|
510
|
+
|
|
383
511
|
// Create the code block that will follow the fence
|
|
384
512
|
currentCodeBlock = document.createElement('pre');
|
|
385
513
|
const codeElement = document.createElement('code');
|
|
386
514
|
currentCodeBlock.appendChild(codeElement);
|
|
387
515
|
currentCodeBlock.className = 'code-block';
|
|
388
|
-
|
|
516
|
+
|
|
389
517
|
// Extract language if present
|
|
390
518
|
const lang = fenceText.slice(3).trim();
|
|
391
519
|
if (lang) {
|
|
392
520
|
codeElement.className = `language-${lang}`;
|
|
393
521
|
}
|
|
394
|
-
|
|
522
|
+
|
|
395
523
|
// Insert code block after the fence div (don't remove the fence)
|
|
396
524
|
container.insertBefore(currentCodeBlock, child.nextSibling);
|
|
397
|
-
|
|
525
|
+
|
|
398
526
|
// Store reference to the code element for adding content
|
|
399
527
|
currentCodeBlock._codeElement = codeElement;
|
|
400
528
|
continue;
|
|
@@ -406,7 +534,7 @@ export class MarkdownParser {
|
|
|
406
534
|
}
|
|
407
535
|
}
|
|
408
536
|
}
|
|
409
|
-
|
|
537
|
+
|
|
410
538
|
// Check if we're in a code block - any div that's not a code fence
|
|
411
539
|
if (inCodeBlock && currentCodeBlock && child.tagName === 'DIV' && !child.querySelector('.code-fence')) {
|
|
412
540
|
const codeElement = currentCodeBlock._codeElement || currentCodeBlock.querySelector('code');
|
|
@@ -422,36 +550,52 @@ export class MarkdownParser {
|
|
|
422
550
|
child.remove();
|
|
423
551
|
continue;
|
|
424
552
|
}
|
|
425
|
-
|
|
553
|
+
|
|
426
554
|
// Check if this div contains a list item
|
|
427
555
|
let listItem = null;
|
|
428
556
|
if (child.tagName === 'DIV') {
|
|
429
557
|
// Look for li inside the div
|
|
430
558
|
listItem = child.querySelector('li');
|
|
431
559
|
}
|
|
432
|
-
|
|
560
|
+
|
|
433
561
|
if (listItem) {
|
|
434
562
|
const isBullet = listItem.classList.contains('bullet-list');
|
|
435
563
|
const isOrdered = listItem.classList.contains('ordered-list');
|
|
436
|
-
|
|
564
|
+
|
|
437
565
|
if (!isBullet && !isOrdered) {
|
|
438
566
|
currentList = null;
|
|
439
567
|
listType = null;
|
|
440
568
|
continue;
|
|
441
569
|
}
|
|
442
|
-
|
|
570
|
+
|
|
443
571
|
const newType = isBullet ? 'ul' : 'ol';
|
|
444
|
-
|
|
572
|
+
|
|
445
573
|
// Start new list or continue current
|
|
446
574
|
if (!currentList || listType !== newType) {
|
|
447
575
|
currentList = document.createElement(newType);
|
|
448
576
|
container.insertBefore(currentList, child);
|
|
449
577
|
listType = newType;
|
|
450
578
|
}
|
|
451
|
-
|
|
579
|
+
|
|
580
|
+
// Extract and preserve indentation from the div before moving the list item
|
|
581
|
+
const indentationNodes = [];
|
|
582
|
+
for (const node of child.childNodes) {
|
|
583
|
+
if (node.nodeType === 3 && node.textContent.match(/^\u00A0+$/)) {
|
|
584
|
+
// This is a text node containing only non-breaking spaces (indentation)
|
|
585
|
+
indentationNodes.push(node.cloneNode(true));
|
|
586
|
+
} else if (node === listItem) {
|
|
587
|
+
break; // Stop when we reach the list item
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Add indentation to the list item
|
|
592
|
+
indentationNodes.forEach(node => {
|
|
593
|
+
listItem.insertBefore(node, listItem.firstChild);
|
|
594
|
+
});
|
|
595
|
+
|
|
452
596
|
// Move the list item to the current list
|
|
453
597
|
currentList.appendChild(listItem);
|
|
454
|
-
|
|
598
|
+
|
|
455
599
|
// Remove the now-empty div wrapper
|
|
456
600
|
child.remove();
|
|
457
601
|
} else {
|
|
@@ -460,7 +604,7 @@ export class MarkdownParser {
|
|
|
460
604
|
listType = null;
|
|
461
605
|
}
|
|
462
606
|
}
|
|
463
|
-
|
|
607
|
+
|
|
464
608
|
return container.innerHTML;
|
|
465
609
|
}
|
|
466
610
|
|
|
@@ -471,25 +615,53 @@ export class MarkdownParser {
|
|
|
471
615
|
*/
|
|
472
616
|
static postProcessHTMLManual(html) {
|
|
473
617
|
let processed = html;
|
|
474
|
-
|
|
618
|
+
|
|
475
619
|
// Process unordered lists
|
|
476
620
|
processed = processed.replace(/((?:<div>(?: )*<li class="bullet-list">.*?<\/li><\/div>\s*)+)/gs, (match) => {
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
621
|
+
const divs = match.match(/<div>(?: )*<li class="bullet-list">.*?<\/li><\/div>/gs) || [];
|
|
622
|
+
if (divs.length > 0) {
|
|
623
|
+
const items = divs.map(div => {
|
|
624
|
+
// Extract indentation and list item
|
|
625
|
+
const indentMatch = div.match(/<div>((?: )*)<li/);
|
|
626
|
+
const listItemMatch = div.match(/<li class="bullet-list">.*?<\/li>/);
|
|
627
|
+
|
|
628
|
+
if (indentMatch && listItemMatch) {
|
|
629
|
+
const indentation = indentMatch[1];
|
|
630
|
+
const listItem = listItemMatch[0];
|
|
631
|
+
// Insert indentation at the start of the list item content
|
|
632
|
+
return listItem.replace(/<li class="bullet-list">/, `<li class="bullet-list">${indentation}`);
|
|
633
|
+
}
|
|
634
|
+
return listItemMatch ? listItemMatch[0] : '';
|
|
635
|
+
}).filter(Boolean);
|
|
636
|
+
|
|
479
637
|
return '<ul>' + items.join('') + '</ul>';
|
|
480
638
|
}
|
|
481
639
|
return match;
|
|
482
640
|
});
|
|
483
|
-
|
|
641
|
+
|
|
484
642
|
// Process ordered lists
|
|
485
643
|
processed = processed.replace(/((?:<div>(?: )*<li class="ordered-list">.*?<\/li><\/div>\s*)+)/gs, (match) => {
|
|
486
|
-
const
|
|
487
|
-
if (
|
|
644
|
+
const divs = match.match(/<div>(?: )*<li class="ordered-list">.*?<\/li><\/div>/gs) || [];
|
|
645
|
+
if (divs.length > 0) {
|
|
646
|
+
const items = divs.map(div => {
|
|
647
|
+
// Extract indentation and list item
|
|
648
|
+
const indentMatch = div.match(/<div>((?: )*)<li/);
|
|
649
|
+
const listItemMatch = div.match(/<li class="ordered-list">.*?<\/li>/);
|
|
650
|
+
|
|
651
|
+
if (indentMatch && listItemMatch) {
|
|
652
|
+
const indentation = indentMatch[1];
|
|
653
|
+
const listItem = listItemMatch[0];
|
|
654
|
+
// Insert indentation at the start of the list item content
|
|
655
|
+
return listItem.replace(/<li class="ordered-list">/, `<li class="ordered-list">${indentation}`);
|
|
656
|
+
}
|
|
657
|
+
return listItemMatch ? listItemMatch[0] : '';
|
|
658
|
+
}).filter(Boolean);
|
|
659
|
+
|
|
488
660
|
return '<ol>' + items.join('') + '</ol>';
|
|
489
661
|
}
|
|
490
662
|
return match;
|
|
491
663
|
});
|
|
492
|
-
|
|
664
|
+
|
|
493
665
|
// Process code blocks - KEEP the fence markers for alignment AND use semantic pre/code
|
|
494
666
|
const codeBlockRegex = /<div><span class="code-fence">(```[^<]*)<\/span><\/div>(.*?)<div><span class="code-fence">(```)<\/span><\/div>/gs;
|
|
495
667
|
processed = processed.replace(codeBlockRegex, (match, openFence, content, closeFence) => {
|
|
@@ -501,20 +673,20 @@ export class MarkdownParser {
|
|
|
501
673
|
.replace(/ /g, ' ');
|
|
502
674
|
return text;
|
|
503
675
|
}).join('\n');
|
|
504
|
-
|
|
676
|
+
|
|
505
677
|
// Extract language from the opening fence
|
|
506
678
|
const lang = openFence.slice(3).trim();
|
|
507
679
|
const langClass = lang ? ` class="language-${lang}"` : '';
|
|
508
|
-
|
|
680
|
+
|
|
509
681
|
// Keep fence markers visible as separate divs, with pre/code block between them
|
|
510
682
|
let result = `<div><span class="code-fence">${openFence}</span></div>`;
|
|
511
683
|
// Content is already escaped, don't double-escape
|
|
512
684
|
result += `<pre class="code-block"><code${langClass}>${codeContent}</code></pre>`;
|
|
513
685
|
result += `<div><span class="code-fence">${closeFence}</span></div>`;
|
|
514
|
-
|
|
686
|
+
|
|
515
687
|
return result;
|
|
516
688
|
});
|
|
517
|
-
|
|
689
|
+
|
|
518
690
|
return processed;
|
|
519
691
|
}
|
|
520
692
|
|
|
@@ -539,7 +711,7 @@ export class MarkdownParser {
|
|
|
539
711
|
let currentPos = 0;
|
|
540
712
|
let lineIndex = 0;
|
|
541
713
|
let lineStart = 0;
|
|
542
|
-
|
|
714
|
+
|
|
543
715
|
for (let i = 0; i < lines.length; i++) {
|
|
544
716
|
const lineLength = lines[i].length;
|
|
545
717
|
if (currentPos + lineLength >= cursorPosition) {
|
|
@@ -549,10 +721,10 @@ export class MarkdownParser {
|
|
|
549
721
|
}
|
|
550
722
|
currentPos += lineLength + 1; // +1 for newline
|
|
551
723
|
}
|
|
552
|
-
|
|
724
|
+
|
|
553
725
|
const currentLine = lines[lineIndex];
|
|
554
726
|
const lineEnd = lineStart + currentLine.length;
|
|
555
|
-
|
|
727
|
+
|
|
556
728
|
// Check for checkbox first (most specific)
|
|
557
729
|
const checkboxMatch = currentLine.match(this.LIST_PATTERNS.checkbox);
|
|
558
730
|
if (checkboxMatch) {
|
|
@@ -568,7 +740,7 @@ export class MarkdownParser {
|
|
|
568
740
|
markerEndPos: lineStart + checkboxMatch[1].length + checkboxMatch[2].length + 5 // indent + "- [ ] "
|
|
569
741
|
};
|
|
570
742
|
}
|
|
571
|
-
|
|
743
|
+
|
|
572
744
|
// Check for bullet list
|
|
573
745
|
const bulletMatch = currentLine.match(this.LIST_PATTERNS.bullet);
|
|
574
746
|
if (bulletMatch) {
|
|
@@ -583,7 +755,7 @@ export class MarkdownParser {
|
|
|
583
755
|
markerEndPos: lineStart + bulletMatch[1].length + bulletMatch[2].length + 1 // indent + marker + space
|
|
584
756
|
};
|
|
585
757
|
}
|
|
586
|
-
|
|
758
|
+
|
|
587
759
|
// Check for numbered list
|
|
588
760
|
const numberedMatch = currentLine.match(this.LIST_PATTERNS.numbered);
|
|
589
761
|
if (numberedMatch) {
|
|
@@ -598,7 +770,7 @@ export class MarkdownParser {
|
|
|
598
770
|
markerEndPos: lineStart + numberedMatch[1].length + numberedMatch[2].length + 2 // indent + number + ". "
|
|
599
771
|
};
|
|
600
772
|
}
|
|
601
|
-
|
|
773
|
+
|
|
602
774
|
// Not in a list
|
|
603
775
|
return {
|
|
604
776
|
inList: false,
|
|
@@ -639,31 +811,31 @@ export class MarkdownParser {
|
|
|
639
811
|
const lines = text.split('\n');
|
|
640
812
|
const numbersByIndent = new Map();
|
|
641
813
|
let inList = false;
|
|
642
|
-
|
|
814
|
+
|
|
643
815
|
const result = lines.map(line => {
|
|
644
816
|
const match = line.match(this.LIST_PATTERNS.numbered);
|
|
645
|
-
|
|
817
|
+
|
|
646
818
|
if (match) {
|
|
647
819
|
const indent = match[1];
|
|
648
820
|
const indentLevel = indent.length;
|
|
649
821
|
const content = match[3];
|
|
650
|
-
|
|
822
|
+
|
|
651
823
|
// If we weren't in a list or indent changed, reset lower levels
|
|
652
824
|
if (!inList) {
|
|
653
825
|
numbersByIndent.clear();
|
|
654
826
|
}
|
|
655
|
-
|
|
827
|
+
|
|
656
828
|
// Get the next number for this indent level
|
|
657
829
|
const currentNumber = (numbersByIndent.get(indentLevel) || 0) + 1;
|
|
658
830
|
numbersByIndent.set(indentLevel, currentNumber);
|
|
659
|
-
|
|
831
|
+
|
|
660
832
|
// Clear deeper indent levels
|
|
661
833
|
for (const [level] of numbersByIndent) {
|
|
662
834
|
if (level > indentLevel) {
|
|
663
835
|
numbersByIndent.delete(level);
|
|
664
836
|
}
|
|
665
837
|
}
|
|
666
|
-
|
|
838
|
+
|
|
667
839
|
inList = true;
|
|
668
840
|
return `${indent}${currentNumber}. ${content}`;
|
|
669
841
|
} else {
|
|
@@ -676,7 +848,7 @@ export class MarkdownParser {
|
|
|
676
848
|
return line;
|
|
677
849
|
}
|
|
678
850
|
});
|
|
679
|
-
|
|
851
|
+
|
|
680
852
|
return result.join('\n');
|
|
681
853
|
}
|
|
682
|
-
}
|
|
854
|
+
}
|