overtype 1.2.2 → 1.2.4
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 +41 -15
- package/dist/overtype.cjs +393 -40
- package/dist/overtype.cjs.map +2 -2
- package/dist/overtype.d.ts +169 -0
- package/dist/overtype.esm.js +393 -40
- package/dist/overtype.esm.js.map +2 -2
- package/dist/overtype.js +398 -40
- package/dist/overtype.js.map +2 -2
- package/dist/overtype.min.js +97 -74
- package/package.json +4 -2
- package/src/link-tooltip.js +16 -16
- package/src/overtype.d.ts +23 -1
- package/src/overtype.js +167 -13
- package/src/parser.js +276 -55
- 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
|
|
@@ -139,6 +139,20 @@ export class MarkdownParser {
|
|
|
139
139
|
return html;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Parse strikethrough text
|
|
144
|
+
* Supports both single (~) and double (~~) tildes, but rejects 3+ tildes
|
|
145
|
+
* @param {string} html - HTML with potential strikethrough markdown
|
|
146
|
+
* @returns {string} HTML with strikethrough styling
|
|
147
|
+
*/
|
|
148
|
+
static parseStrikethrough(html) {
|
|
149
|
+
// Double tilde strikethrough: ~~text~~ (but not if part of 3+ tildes)
|
|
150
|
+
html = html.replace(/(?<!~)~~(?!~)(.+?)(?<!~)~~(?!~)/g, '<del><span class="syntax-marker">~~</span>$1<span class="syntax-marker">~~</span></del>');
|
|
151
|
+
// Single tilde strikethrough: ~text~ (but not if part of 2+ tildes on either side)
|
|
152
|
+
html = html.replace(/(?<!~)~(?!~)(.+?)(?<!~)~(?!~)/g, '<del><span class="syntax-marker">~</span>$1<span class="syntax-marker">~</span></del>');
|
|
153
|
+
return html;
|
|
154
|
+
}
|
|
155
|
+
|
|
142
156
|
/**
|
|
143
157
|
* Parse inline code
|
|
144
158
|
* @param {string} html - HTML with potential code markdown
|
|
@@ -165,7 +179,7 @@ export class MarkdownParser {
|
|
|
165
179
|
// Trim whitespace and convert to lowercase for protocol check
|
|
166
180
|
const trimmed = url.trim();
|
|
167
181
|
const lower = trimmed.toLowerCase();
|
|
168
|
-
|
|
182
|
+
|
|
169
183
|
// Allow safe protocols
|
|
170
184
|
const safeProtocols = [
|
|
171
185
|
'http://',
|
|
@@ -174,22 +188,22 @@ export class MarkdownParser {
|
|
|
174
188
|
'ftp://',
|
|
175
189
|
'ftps://'
|
|
176
190
|
];
|
|
177
|
-
|
|
191
|
+
|
|
178
192
|
// Check if URL starts with a safe protocol
|
|
179
193
|
const hasSafeProtocol = safeProtocols.some(protocol => lower.startsWith(protocol));
|
|
180
|
-
|
|
194
|
+
|
|
181
195
|
// Allow relative URLs (starting with / or # or no protocol)
|
|
182
|
-
const isRelative = trimmed.startsWith('/') ||
|
|
183
|
-
trimmed.startsWith('#') ||
|
|
196
|
+
const isRelative = trimmed.startsWith('/') ||
|
|
197
|
+
trimmed.startsWith('#') ||
|
|
184
198
|
trimmed.startsWith('?') ||
|
|
185
199
|
trimmed.startsWith('.') ||
|
|
186
200
|
(!trimmed.includes(':') && !trimmed.includes('//'));
|
|
187
|
-
|
|
201
|
+
|
|
188
202
|
// If safe protocol or relative URL, return as-is
|
|
189
203
|
if (hasSafeProtocol || isRelative) {
|
|
190
204
|
return url;
|
|
191
205
|
}
|
|
192
|
-
|
|
206
|
+
|
|
193
207
|
// Block dangerous protocols (javascript:, data:, vbscript:, etc.)
|
|
194
208
|
return '#';
|
|
195
209
|
}
|
|
@@ -246,6 +260,7 @@ export class MarkdownParser {
|
|
|
246
260
|
});
|
|
247
261
|
|
|
248
262
|
// Process other inline elements on text with placeholders
|
|
263
|
+
html = this.parseStrikethrough(html);
|
|
249
264
|
html = this.parseBold(html);
|
|
250
265
|
html = this.parseItalic(html);
|
|
251
266
|
|
|
@@ -264,33 +279,33 @@ export class MarkdownParser {
|
|
|
264
279
|
*/
|
|
265
280
|
static parseLine(line) {
|
|
266
281
|
let html = this.escapeHtml(line);
|
|
267
|
-
|
|
282
|
+
|
|
268
283
|
// Preserve indentation
|
|
269
284
|
html = this.preserveIndentation(html, line);
|
|
270
|
-
|
|
285
|
+
|
|
271
286
|
// Check for block elements first
|
|
272
287
|
const horizontalRule = this.parseHorizontalRule(html);
|
|
273
288
|
if (horizontalRule) return horizontalRule;
|
|
274
|
-
|
|
289
|
+
|
|
275
290
|
const codeBlock = this.parseCodeBlock(html);
|
|
276
291
|
if (codeBlock) return codeBlock;
|
|
277
|
-
|
|
292
|
+
|
|
278
293
|
// Parse block elements
|
|
279
294
|
html = this.parseHeader(html);
|
|
280
295
|
html = this.parseBlockquote(html);
|
|
281
296
|
html = this.parseBulletList(html);
|
|
282
297
|
html = this.parseNumberedList(html);
|
|
283
|
-
|
|
298
|
+
|
|
284
299
|
// Parse inline elements
|
|
285
300
|
html = this.parseInlineElements(html);
|
|
286
|
-
|
|
301
|
+
|
|
287
302
|
// Wrap in div to maintain line structure
|
|
288
303
|
if (html.trim() === '') {
|
|
289
304
|
// Intentionally use for empty lines to maintain vertical spacing
|
|
290
305
|
// This causes a 0->1 character count difference but preserves visual alignment
|
|
291
306
|
return '<div> </div>';
|
|
292
307
|
}
|
|
293
|
-
|
|
308
|
+
|
|
294
309
|
return `<div>${html}</div>`;
|
|
295
310
|
}
|
|
296
311
|
|
|
@@ -304,17 +319,17 @@ export class MarkdownParser {
|
|
|
304
319
|
static parse(text, activeLine = -1, showActiveLineRaw = false) {
|
|
305
320
|
// Reset link counter for each parse
|
|
306
321
|
this.resetLinkIndex();
|
|
307
|
-
|
|
322
|
+
|
|
308
323
|
const lines = text.split('\n');
|
|
309
324
|
let inCodeBlock = false;
|
|
310
|
-
|
|
325
|
+
|
|
311
326
|
const parsedLines = lines.map((line, index) => {
|
|
312
327
|
// Show raw markdown on active line if requested
|
|
313
328
|
if (showActiveLineRaw && index === activeLine) {
|
|
314
329
|
const content = this.escapeHtml(line) || ' ';
|
|
315
330
|
return `<div class="raw-line">${content}</div>`;
|
|
316
331
|
}
|
|
317
|
-
|
|
332
|
+
|
|
318
333
|
// Check if this line is a code fence
|
|
319
334
|
const codeFenceRegex = /^```[^`]*$/;
|
|
320
335
|
if (codeFenceRegex.test(line)) {
|
|
@@ -322,21 +337,21 @@ export class MarkdownParser {
|
|
|
322
337
|
// Parse fence markers normally to get styled output
|
|
323
338
|
return this.parseLine(line);
|
|
324
339
|
}
|
|
325
|
-
|
|
340
|
+
|
|
326
341
|
// If we're inside a code block, don't parse as markdown
|
|
327
342
|
if (inCodeBlock) {
|
|
328
343
|
const escaped = this.escapeHtml(line);
|
|
329
344
|
const indented = this.preserveIndentation(escaped, line);
|
|
330
345
|
return `<div>${indented || ' '}</div>`;
|
|
331
346
|
}
|
|
332
|
-
|
|
347
|
+
|
|
333
348
|
// Otherwise, parse the markdown normally
|
|
334
349
|
return this.parseLine(line);
|
|
335
350
|
});
|
|
336
|
-
|
|
351
|
+
|
|
337
352
|
// Join without newlines to prevent extra spacing
|
|
338
353
|
const html = parsedLines.join('');
|
|
339
|
-
|
|
354
|
+
|
|
340
355
|
// Apply post-processing for list consolidation
|
|
341
356
|
return this.postProcessHTML(html);
|
|
342
357
|
}
|
|
@@ -352,25 +367,25 @@ export class MarkdownParser {
|
|
|
352
367
|
// In Node.js environment - do manual post-processing
|
|
353
368
|
return this.postProcessHTMLManual(html);
|
|
354
369
|
}
|
|
355
|
-
|
|
370
|
+
|
|
356
371
|
// Parse HTML string into DOM
|
|
357
372
|
const container = document.createElement('div');
|
|
358
373
|
container.innerHTML = html;
|
|
359
|
-
|
|
374
|
+
|
|
360
375
|
let currentList = null;
|
|
361
376
|
let listType = null;
|
|
362
377
|
let currentCodeBlock = null;
|
|
363
378
|
let inCodeBlock = false;
|
|
364
|
-
|
|
379
|
+
|
|
365
380
|
// Process all direct children - need to be careful with live NodeList
|
|
366
381
|
const children = Array.from(container.children);
|
|
367
|
-
|
|
382
|
+
|
|
368
383
|
for (let i = 0; i < children.length; i++) {
|
|
369
384
|
const child = children[i];
|
|
370
|
-
|
|
385
|
+
|
|
371
386
|
// Skip if child was already processed/removed
|
|
372
387
|
if (!child.parentNode) continue;
|
|
373
|
-
|
|
388
|
+
|
|
374
389
|
// Check for code fence start/end
|
|
375
390
|
const codeFence = child.querySelector('.code-fence');
|
|
376
391
|
if (codeFence) {
|
|
@@ -379,22 +394,22 @@ export class MarkdownParser {
|
|
|
379
394
|
if (!inCodeBlock) {
|
|
380
395
|
// Start of code block - keep fence visible, then add pre/code
|
|
381
396
|
inCodeBlock = true;
|
|
382
|
-
|
|
397
|
+
|
|
383
398
|
// Create the code block that will follow the fence
|
|
384
399
|
currentCodeBlock = document.createElement('pre');
|
|
385
400
|
const codeElement = document.createElement('code');
|
|
386
401
|
currentCodeBlock.appendChild(codeElement);
|
|
387
402
|
currentCodeBlock.className = 'code-block';
|
|
388
|
-
|
|
403
|
+
|
|
389
404
|
// Extract language if present
|
|
390
405
|
const lang = fenceText.slice(3).trim();
|
|
391
406
|
if (lang) {
|
|
392
407
|
codeElement.className = `language-${lang}`;
|
|
393
408
|
}
|
|
394
|
-
|
|
409
|
+
|
|
395
410
|
// Insert code block after the fence div (don't remove the fence)
|
|
396
411
|
container.insertBefore(currentCodeBlock, child.nextSibling);
|
|
397
|
-
|
|
412
|
+
|
|
398
413
|
// Store reference to the code element for adding content
|
|
399
414
|
currentCodeBlock._codeElement = codeElement;
|
|
400
415
|
continue;
|
|
@@ -406,7 +421,7 @@ export class MarkdownParser {
|
|
|
406
421
|
}
|
|
407
422
|
}
|
|
408
423
|
}
|
|
409
|
-
|
|
424
|
+
|
|
410
425
|
// Check if we're in a code block - any div that's not a code fence
|
|
411
426
|
if (inCodeBlock && currentCodeBlock && child.tagName === 'DIV' && !child.querySelector('.code-fence')) {
|
|
412
427
|
const codeElement = currentCodeBlock._codeElement || currentCodeBlock.querySelector('code');
|
|
@@ -422,36 +437,52 @@ export class MarkdownParser {
|
|
|
422
437
|
child.remove();
|
|
423
438
|
continue;
|
|
424
439
|
}
|
|
425
|
-
|
|
440
|
+
|
|
426
441
|
// Check if this div contains a list item
|
|
427
442
|
let listItem = null;
|
|
428
443
|
if (child.tagName === 'DIV') {
|
|
429
444
|
// Look for li inside the div
|
|
430
445
|
listItem = child.querySelector('li');
|
|
431
446
|
}
|
|
432
|
-
|
|
447
|
+
|
|
433
448
|
if (listItem) {
|
|
434
449
|
const isBullet = listItem.classList.contains('bullet-list');
|
|
435
450
|
const isOrdered = listItem.classList.contains('ordered-list');
|
|
436
|
-
|
|
451
|
+
|
|
437
452
|
if (!isBullet && !isOrdered) {
|
|
438
453
|
currentList = null;
|
|
439
454
|
listType = null;
|
|
440
455
|
continue;
|
|
441
456
|
}
|
|
442
|
-
|
|
457
|
+
|
|
443
458
|
const newType = isBullet ? 'ul' : 'ol';
|
|
444
|
-
|
|
459
|
+
|
|
445
460
|
// Start new list or continue current
|
|
446
461
|
if (!currentList || listType !== newType) {
|
|
447
462
|
currentList = document.createElement(newType);
|
|
448
463
|
container.insertBefore(currentList, child);
|
|
449
464
|
listType = newType;
|
|
450
465
|
}
|
|
451
|
-
|
|
466
|
+
|
|
467
|
+
// Extract and preserve indentation from the div before moving the list item
|
|
468
|
+
const indentationNodes = [];
|
|
469
|
+
for (const node of child.childNodes) {
|
|
470
|
+
if (node.nodeType === 3 && node.textContent.match(/^\u00A0+$/)) {
|
|
471
|
+
// This is a text node containing only non-breaking spaces (indentation)
|
|
472
|
+
indentationNodes.push(node.cloneNode(true));
|
|
473
|
+
} else if (node === listItem) {
|
|
474
|
+
break; // Stop when we reach the list item
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Add indentation to the list item
|
|
479
|
+
indentationNodes.forEach(node => {
|
|
480
|
+
listItem.insertBefore(node, listItem.firstChild);
|
|
481
|
+
});
|
|
482
|
+
|
|
452
483
|
// Move the list item to the current list
|
|
453
484
|
currentList.appendChild(listItem);
|
|
454
|
-
|
|
485
|
+
|
|
455
486
|
// Remove the now-empty div wrapper
|
|
456
487
|
child.remove();
|
|
457
488
|
} else {
|
|
@@ -460,7 +491,7 @@ export class MarkdownParser {
|
|
|
460
491
|
listType = null;
|
|
461
492
|
}
|
|
462
493
|
}
|
|
463
|
-
|
|
494
|
+
|
|
464
495
|
return container.innerHTML;
|
|
465
496
|
}
|
|
466
497
|
|
|
@@ -471,25 +502,53 @@ export class MarkdownParser {
|
|
|
471
502
|
*/
|
|
472
503
|
static postProcessHTMLManual(html) {
|
|
473
504
|
let processed = html;
|
|
474
|
-
|
|
505
|
+
|
|
475
506
|
// Process unordered lists
|
|
476
507
|
processed = processed.replace(/((?:<div>(?: )*<li class="bullet-list">.*?<\/li><\/div>\s*)+)/gs, (match) => {
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
508
|
+
const divs = match.match(/<div>(?: )*<li class="bullet-list">.*?<\/li><\/div>/gs) || [];
|
|
509
|
+
if (divs.length > 0) {
|
|
510
|
+
const items = divs.map(div => {
|
|
511
|
+
// Extract indentation and list item
|
|
512
|
+
const indentMatch = div.match(/<div>((?: )*)<li/);
|
|
513
|
+
const listItemMatch = div.match(/<li class="bullet-list">.*?<\/li>/);
|
|
514
|
+
|
|
515
|
+
if (indentMatch && listItemMatch) {
|
|
516
|
+
const indentation = indentMatch[1];
|
|
517
|
+
const listItem = listItemMatch[0];
|
|
518
|
+
// Insert indentation at the start of the list item content
|
|
519
|
+
return listItem.replace(/<li class="bullet-list">/, `<li class="bullet-list">${indentation}`);
|
|
520
|
+
}
|
|
521
|
+
return listItemMatch ? listItemMatch[0] : '';
|
|
522
|
+
}).filter(Boolean);
|
|
523
|
+
|
|
479
524
|
return '<ul>' + items.join('') + '</ul>';
|
|
480
525
|
}
|
|
481
526
|
return match;
|
|
482
527
|
});
|
|
483
|
-
|
|
528
|
+
|
|
484
529
|
// Process ordered lists
|
|
485
530
|
processed = processed.replace(/((?:<div>(?: )*<li class="ordered-list">.*?<\/li><\/div>\s*)+)/gs, (match) => {
|
|
486
|
-
const
|
|
487
|
-
if (
|
|
531
|
+
const divs = match.match(/<div>(?: )*<li class="ordered-list">.*?<\/li><\/div>/gs) || [];
|
|
532
|
+
if (divs.length > 0) {
|
|
533
|
+
const items = divs.map(div => {
|
|
534
|
+
// Extract indentation and list item
|
|
535
|
+
const indentMatch = div.match(/<div>((?: )*)<li/);
|
|
536
|
+
const listItemMatch = div.match(/<li class="ordered-list">.*?<\/li>/);
|
|
537
|
+
|
|
538
|
+
if (indentMatch && listItemMatch) {
|
|
539
|
+
const indentation = indentMatch[1];
|
|
540
|
+
const listItem = listItemMatch[0];
|
|
541
|
+
// Insert indentation at the start of the list item content
|
|
542
|
+
return listItem.replace(/<li class="ordered-list">/, `<li class="ordered-list">${indentation}`);
|
|
543
|
+
}
|
|
544
|
+
return listItemMatch ? listItemMatch[0] : '';
|
|
545
|
+
}).filter(Boolean);
|
|
546
|
+
|
|
488
547
|
return '<ol>' + items.join('') + '</ol>';
|
|
489
548
|
}
|
|
490
549
|
return match;
|
|
491
550
|
});
|
|
492
|
-
|
|
551
|
+
|
|
493
552
|
// Process code blocks - KEEP the fence markers for alignment AND use semantic pre/code
|
|
494
553
|
const codeBlockRegex = /<div><span class="code-fence">(```[^<]*)<\/span><\/div>(.*?)<div><span class="code-fence">(```)<\/span><\/div>/gs;
|
|
495
554
|
processed = processed.replace(codeBlockRegex, (match, openFence, content, closeFence) => {
|
|
@@ -501,20 +560,182 @@ export class MarkdownParser {
|
|
|
501
560
|
.replace(/ /g, ' ');
|
|
502
561
|
return text;
|
|
503
562
|
}).join('\n');
|
|
504
|
-
|
|
563
|
+
|
|
505
564
|
// Extract language from the opening fence
|
|
506
565
|
const lang = openFence.slice(3).trim();
|
|
507
566
|
const langClass = lang ? ` class="language-${lang}"` : '';
|
|
508
|
-
|
|
567
|
+
|
|
509
568
|
// Keep fence markers visible as separate divs, with pre/code block between them
|
|
510
569
|
let result = `<div><span class="code-fence">${openFence}</span></div>`;
|
|
511
570
|
// Content is already escaped, don't double-escape
|
|
512
571
|
result += `<pre class="code-block"><code${langClass}>${codeContent}</code></pre>`;
|
|
513
572
|
result += `<div><span class="code-fence">${closeFence}</span></div>`;
|
|
514
|
-
|
|
573
|
+
|
|
515
574
|
return result;
|
|
516
575
|
});
|
|
517
|
-
|
|
576
|
+
|
|
518
577
|
return processed;
|
|
519
578
|
}
|
|
520
|
-
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* List pattern definitions
|
|
582
|
+
*/
|
|
583
|
+
static LIST_PATTERNS = {
|
|
584
|
+
bullet: /^(\s*)([-*+])\s+(.*)$/,
|
|
585
|
+
numbered: /^(\s*)(\d+)\.\s+(.*)$/,
|
|
586
|
+
checkbox: /^(\s*)-\s+\[([ x])\]\s+(.*)$/
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Get list context at cursor position
|
|
591
|
+
* @param {string} text - Full text content
|
|
592
|
+
* @param {number} cursorPosition - Current cursor position
|
|
593
|
+
* @returns {Object} List context information
|
|
594
|
+
*/
|
|
595
|
+
static getListContext(text, cursorPosition) {
|
|
596
|
+
// Find the line containing the cursor
|
|
597
|
+
const lines = text.split('\n');
|
|
598
|
+
let currentPos = 0;
|
|
599
|
+
let lineIndex = 0;
|
|
600
|
+
let lineStart = 0;
|
|
601
|
+
|
|
602
|
+
for (let i = 0; i < lines.length; i++) {
|
|
603
|
+
const lineLength = lines[i].length;
|
|
604
|
+
if (currentPos + lineLength >= cursorPosition) {
|
|
605
|
+
lineIndex = i;
|
|
606
|
+
lineStart = currentPos;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
currentPos += lineLength + 1; // +1 for newline
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const currentLine = lines[lineIndex];
|
|
613
|
+
const lineEnd = lineStart + currentLine.length;
|
|
614
|
+
|
|
615
|
+
// Check for checkbox first (most specific)
|
|
616
|
+
const checkboxMatch = currentLine.match(this.LIST_PATTERNS.checkbox);
|
|
617
|
+
if (checkboxMatch) {
|
|
618
|
+
return {
|
|
619
|
+
inList: true,
|
|
620
|
+
listType: 'checkbox',
|
|
621
|
+
indent: checkboxMatch[1],
|
|
622
|
+
marker: '-',
|
|
623
|
+
checked: checkboxMatch[2] === 'x',
|
|
624
|
+
content: checkboxMatch[3],
|
|
625
|
+
lineStart,
|
|
626
|
+
lineEnd,
|
|
627
|
+
markerEndPos: lineStart + checkboxMatch[1].length + checkboxMatch[2].length + 5 // indent + "- [ ] "
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Check for bullet list
|
|
632
|
+
const bulletMatch = currentLine.match(this.LIST_PATTERNS.bullet);
|
|
633
|
+
if (bulletMatch) {
|
|
634
|
+
return {
|
|
635
|
+
inList: true,
|
|
636
|
+
listType: 'bullet',
|
|
637
|
+
indent: bulletMatch[1],
|
|
638
|
+
marker: bulletMatch[2],
|
|
639
|
+
content: bulletMatch[3],
|
|
640
|
+
lineStart,
|
|
641
|
+
lineEnd,
|
|
642
|
+
markerEndPos: lineStart + bulletMatch[1].length + bulletMatch[2].length + 1 // indent + marker + space
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Check for numbered list
|
|
647
|
+
const numberedMatch = currentLine.match(this.LIST_PATTERNS.numbered);
|
|
648
|
+
if (numberedMatch) {
|
|
649
|
+
return {
|
|
650
|
+
inList: true,
|
|
651
|
+
listType: 'numbered',
|
|
652
|
+
indent: numberedMatch[1],
|
|
653
|
+
marker: parseInt(numberedMatch[2]),
|
|
654
|
+
content: numberedMatch[3],
|
|
655
|
+
lineStart,
|
|
656
|
+
lineEnd,
|
|
657
|
+
markerEndPos: lineStart + numberedMatch[1].length + numberedMatch[2].length + 2 // indent + number + ". "
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Not in a list
|
|
662
|
+
return {
|
|
663
|
+
inList: false,
|
|
664
|
+
listType: null,
|
|
665
|
+
indent: '',
|
|
666
|
+
marker: null,
|
|
667
|
+
content: currentLine,
|
|
668
|
+
lineStart,
|
|
669
|
+
lineEnd,
|
|
670
|
+
markerEndPos: lineStart
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Create a new list item based on context
|
|
676
|
+
* @param {Object} context - List context from getListContext
|
|
677
|
+
* @returns {string} New list item text
|
|
678
|
+
*/
|
|
679
|
+
static createNewListItem(context) {
|
|
680
|
+
switch (context.listType) {
|
|
681
|
+
case 'bullet':
|
|
682
|
+
return `${context.indent}${context.marker} `;
|
|
683
|
+
case 'numbered':
|
|
684
|
+
return `${context.indent}${context.marker + 1}. `;
|
|
685
|
+
case 'checkbox':
|
|
686
|
+
return `${context.indent}- [ ] `;
|
|
687
|
+
default:
|
|
688
|
+
return '';
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Renumber all numbered lists in text
|
|
694
|
+
* @param {string} text - Text containing numbered lists
|
|
695
|
+
* @returns {string} Text with renumbered lists
|
|
696
|
+
*/
|
|
697
|
+
static renumberLists(text) {
|
|
698
|
+
const lines = text.split('\n');
|
|
699
|
+
const numbersByIndent = new Map();
|
|
700
|
+
let inList = false;
|
|
701
|
+
|
|
702
|
+
const result = lines.map(line => {
|
|
703
|
+
const match = line.match(this.LIST_PATTERNS.numbered);
|
|
704
|
+
|
|
705
|
+
if (match) {
|
|
706
|
+
const indent = match[1];
|
|
707
|
+
const indentLevel = indent.length;
|
|
708
|
+
const content = match[3];
|
|
709
|
+
|
|
710
|
+
// If we weren't in a list or indent changed, reset lower levels
|
|
711
|
+
if (!inList) {
|
|
712
|
+
numbersByIndent.clear();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Get the next number for this indent level
|
|
716
|
+
const currentNumber = (numbersByIndent.get(indentLevel) || 0) + 1;
|
|
717
|
+
numbersByIndent.set(indentLevel, currentNumber);
|
|
718
|
+
|
|
719
|
+
// Clear deeper indent levels
|
|
720
|
+
for (const [level] of numbersByIndent) {
|
|
721
|
+
if (level > indentLevel) {
|
|
722
|
+
numbersByIndent.delete(level);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
inList = true;
|
|
727
|
+
return `${indent}${currentNumber}. ${content}`;
|
|
728
|
+
} else {
|
|
729
|
+
// Not a numbered list item
|
|
730
|
+
if (line.trim() === '' || !line.match(/^\s/)) {
|
|
731
|
+
// Empty line or non-indented line breaks the list
|
|
732
|
+
inList = false;
|
|
733
|
+
numbersByIndent.clear();
|
|
734
|
+
}
|
|
735
|
+
return line;
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
return result.join('\n');
|
|
740
|
+
}
|
|
741
|
+
}
|
package/src/styles.js
CHANGED
|
@@ -325,6 +325,14 @@ export function generateStyles(options = {}) {
|
|
|
325
325
|
font-style: italic !important;
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
/* Strikethrough text */
|
|
329
|
+
.overtype-wrapper .overtype-preview del {
|
|
330
|
+
color: var(--del, #ee964b) !important;
|
|
331
|
+
text-decoration: line-through !important;
|
|
332
|
+
text-decoration-color: var(--del, #ee964b) !important;
|
|
333
|
+
text-decoration-thickness: 1px !important;
|
|
334
|
+
}
|
|
335
|
+
|
|
328
336
|
/* Inline code */
|
|
329
337
|
.overtype-wrapper .overtype-preview code {
|
|
330
338
|
background: var(--code-bg, rgba(244, 211, 94, 0.4)) !important;
|
|
@@ -468,10 +476,10 @@ export function generateStyles(options = {}) {
|
|
|
468
476
|
height: 8px !important;
|
|
469
477
|
background: #4caf50 !important;
|
|
470
478
|
border-radius: 50% !important;
|
|
471
|
-
animation: pulse 2s infinite !important;
|
|
479
|
+
animation: overtype-pulse 2s infinite !important;
|
|
472
480
|
}
|
|
473
481
|
|
|
474
|
-
@keyframes pulse {
|
|
482
|
+
@keyframes overtype-pulse {
|
|
475
483
|
0%, 100% { opacity: 1; transform: scale(1); }
|
|
476
484
|
50% { opacity: 0.6; transform: scale(1.2); }
|
|
477
485
|
}
|
|
@@ -479,19 +487,19 @@ export function generateStyles(options = {}) {
|
|
|
479
487
|
|
|
480
488
|
/* Toolbar Styles */
|
|
481
489
|
.overtype-toolbar {
|
|
482
|
-
display: flex;
|
|
483
|
-
align-items: center;
|
|
484
|
-
gap: 4px;
|
|
490
|
+
display: flex !important;
|
|
491
|
+
align-items: center !important;
|
|
492
|
+
gap: 4px !important;
|
|
485
493
|
padding: 8px !important; /* Override reset */
|
|
486
494
|
background: var(--toolbar-bg, var(--bg-primary, #f8f9fa)) !important; /* Override reset */
|
|
487
495
|
overflow-x: auto !important; /* Allow horizontal scrolling */
|
|
488
496
|
overflow-y: hidden !important; /* Hide vertical overflow */
|
|
489
|
-
-webkit-overflow-scrolling: touch;
|
|
490
|
-
flex-shrink: 0;
|
|
497
|
+
-webkit-overflow-scrolling: touch !important;
|
|
498
|
+
flex-shrink: 0 !important;
|
|
491
499
|
height: auto !important;
|
|
492
500
|
grid-row: 1 !important; /* Always first row in grid */
|
|
493
501
|
position: relative !important; /* Override reset */
|
|
494
|
-
z-index: 100; /* Ensure toolbar is above wrapper */
|
|
502
|
+
z-index: 100 !important; /* Ensure toolbar is above wrapper */
|
|
495
503
|
scrollbar-width: thin; /* Thin scrollbar on Firefox */
|
|
496
504
|
}
|
|
497
505
|
|