quikdown 1.1.1 → 1.2.2
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 +35 -3
- package/dist/quikdown.cjs +5 -5
- package/dist/quikdown.dark.css +1 -1
- package/dist/quikdown.esm.js +5 -5
- package/dist/quikdown.esm.min.js +2 -2
- package/dist/quikdown.esm.min.js.map +1 -1
- package/dist/quikdown.light.css +1 -1
- package/dist/quikdown.umd.js +5 -5
- package/dist/quikdown.umd.min.js +2 -2
- package/dist/quikdown.umd.min.js.map +1 -1
- package/dist/quikdown_ast.cjs +513 -0
- package/dist/quikdown_ast.d.ts +227 -0
- package/dist/quikdown_ast.esm.js +511 -0
- package/dist/quikdown_ast.esm.min.js +8 -0
- package/dist/quikdown_ast.esm.min.js.map +1 -0
- package/dist/quikdown_ast.umd.js +519 -0
- package/dist/quikdown_ast.umd.min.js +8 -0
- package/dist/quikdown_ast.umd.min.js.map +1 -0
- package/dist/quikdown_ast_html.cjs +1058 -0
- package/dist/quikdown_ast_html.d.ts +68 -0
- package/dist/quikdown_ast_html.esm.js +1056 -0
- package/dist/quikdown_ast_html.esm.min.js +8 -0
- package/dist/quikdown_ast_html.esm.min.js.map +1 -0
- package/dist/quikdown_ast_html.umd.js +1064 -0
- package/dist/quikdown_ast_html.umd.min.js +8 -0
- package/dist/quikdown_ast_html.umd.min.js.map +1 -0
- package/dist/quikdown_bd.cjs +12 -12
- package/dist/quikdown_bd.esm.js +12 -12
- package/dist/quikdown_bd.esm.min.js +2 -2
- package/dist/quikdown_bd.esm.min.js.map +1 -1
- package/dist/quikdown_bd.umd.js +12 -12
- package/dist/quikdown_bd.umd.min.js +2 -2
- package/dist/quikdown_bd.umd.min.js.map +1 -1
- package/dist/quikdown_edit.cjs +434 -58
- package/dist/quikdown_edit.d.ts +110 -132
- package/dist/quikdown_edit.esm.js +434 -58
- package/dist/quikdown_edit.esm.min.js +3 -3
- package/dist/quikdown_edit.esm.min.js.map +1 -1
- package/dist/quikdown_edit.umd.js +434 -58
- package/dist/quikdown_edit.umd.min.js +3 -3
- package/dist/quikdown_edit.umd.min.js.map +1 -1
- package/dist/quikdown_json.cjs +556 -0
- package/dist/quikdown_json.d.ts +48 -0
- package/dist/quikdown_json.esm.js +554 -0
- package/dist/quikdown_json.esm.min.js +8 -0
- package/dist/quikdown_json.esm.min.js.map +1 -0
- package/dist/quikdown_json.umd.js +562 -0
- package/dist/quikdown_json.umd.min.js +8 -0
- package/dist/quikdown_json.umd.min.js.map +1 -0
- package/dist/quikdown_yaml.cjs +717 -0
- package/dist/quikdown_yaml.d.ts +51 -0
- package/dist/quikdown_yaml.esm.js +715 -0
- package/dist/quikdown_yaml.esm.min.js +8 -0
- package/dist/quikdown_yaml.esm.min.js.map +1 -0
- package/dist/quikdown_yaml.umd.js +723 -0
- package/dist/quikdown_yaml.umd.min.js +8 -0
- package/dist/quikdown_yaml.umd.min.js.map +1 -0
- package/package.json +91 -38
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quikdown Editor - Drop-in Markdown Parser
|
|
3
|
-
* @version 1.
|
|
3
|
+
* @version 1.2.2
|
|
4
4
|
* @license BSD-2-Clause
|
|
5
5
|
* @copyright DeftIO 2025
|
|
6
6
|
*/
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
// Version will be injected at build time
|
|
27
|
-
const quikdownVersion = '1.
|
|
27
|
+
const quikdownVersion = '1.2.2';
|
|
28
28
|
|
|
29
29
|
// Constants for reuse
|
|
30
30
|
const CLASS_PREFIX = 'quikdown-';
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
html = '<p>' + html + '</p>';
|
|
275
275
|
} else {
|
|
276
276
|
// Standard: two spaces at end of line for line breaks
|
|
277
|
-
html = html.replace(/
|
|
277
|
+
html = html.replace(/ {2}$/gm, `<br${getAttr('br')}>`);
|
|
278
278
|
|
|
279
279
|
// Paragraphs (double newlines)
|
|
280
280
|
// Don't add </p> after block elements (they're not in paragraphs)
|
|
@@ -303,7 +303,7 @@
|
|
|
303
303
|
[/(<\/table>)<\/p>/g, '$1'],
|
|
304
304
|
[/<p>(<pre[^>]*>)/g, '$1'],
|
|
305
305
|
[/(<\/pre>)<\/p>/g, '$1'],
|
|
306
|
-
[new RegExp(`<p>(${PLACEHOLDER_CB}\\d+§)
|
|
306
|
+
[new RegExp(`<p>(${PLACEHOLDER_CB}\\d+§)</p>`, 'g'), '$1']
|
|
307
307
|
];
|
|
308
308
|
|
|
309
309
|
cleanupPatterns.forEach(([pattern, replacement]) => {
|
|
@@ -509,7 +509,7 @@
|
|
|
509
509
|
|
|
510
510
|
const lines = text.split('\n');
|
|
511
511
|
const result = [];
|
|
512
|
-
|
|
512
|
+
const listStack = []; // Track nested lists
|
|
513
513
|
|
|
514
514
|
// Helper to escape HTML for data-qd attributes
|
|
515
515
|
const escapeHtml = (text) => text.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]);
|
|
@@ -719,7 +719,7 @@
|
|
|
719
719
|
|
|
720
720
|
// Process children with context
|
|
721
721
|
let childContent = '';
|
|
722
|
-
for (
|
|
722
|
+
for (const child of node.childNodes) {
|
|
723
723
|
childContent += walkNode(child, { parentTag: tag, ...parentContext });
|
|
724
724
|
}
|
|
725
725
|
|
|
@@ -953,7 +953,7 @@
|
|
|
953
953
|
let index = 1;
|
|
954
954
|
const indent = ' '.repeat(depth);
|
|
955
955
|
|
|
956
|
-
for (
|
|
956
|
+
for (const child of listNode.children) {
|
|
957
957
|
if (child.tagName !== 'LI') continue;
|
|
958
958
|
|
|
959
959
|
const dataQd = child.getAttribute('data-qd');
|
|
@@ -966,7 +966,7 @@
|
|
|
966
966
|
marker = '-';
|
|
967
967
|
// Get text without the checkbox
|
|
968
968
|
let text = '';
|
|
969
|
-
for (
|
|
969
|
+
for (const node of child.childNodes) {
|
|
970
970
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
971
971
|
text += node.textContent;
|
|
972
972
|
} else if (node.tagName && node.tagName !== 'INPUT') {
|
|
@@ -977,7 +977,7 @@
|
|
|
977
977
|
} else {
|
|
978
978
|
let itemContent = '';
|
|
979
979
|
|
|
980
|
-
for (
|
|
980
|
+
for (const node of child.childNodes) {
|
|
981
981
|
if (node.tagName === 'UL' || node.tagName === 'OL') {
|
|
982
982
|
itemContent += walkList(node, node.tagName === 'OL', depth + 1);
|
|
983
983
|
} else {
|
|
@@ -1006,7 +1006,7 @@
|
|
|
1006
1006
|
const headerRow = thead.querySelector('tr');
|
|
1007
1007
|
if (headerRow) {
|
|
1008
1008
|
const headers = [];
|
|
1009
|
-
for (
|
|
1009
|
+
for (const th of headerRow.querySelectorAll('th')) {
|
|
1010
1010
|
headers.push(th.textContent.trim());
|
|
1011
1011
|
}
|
|
1012
1012
|
result += '| ' + headers.join(' | ') + ' |\n';
|
|
@@ -1025,9 +1025,9 @@
|
|
|
1025
1025
|
// Process body
|
|
1026
1026
|
const tbody = table.querySelector('tbody');
|
|
1027
1027
|
if (tbody) {
|
|
1028
|
-
for (
|
|
1028
|
+
for (const row of tbody.querySelectorAll('tr')) {
|
|
1029
1029
|
const cells = [];
|
|
1030
|
-
for (
|
|
1030
|
+
for (const td of row.querySelectorAll('td')) {
|
|
1031
1031
|
cells.push(td.textContent.trim());
|
|
1032
1032
|
}
|
|
1033
1033
|
if (cells.length > 0) {
|
|
@@ -1931,7 +1931,7 @@
|
|
|
1931
1931
|
// First try baseVal.value (works for absolute units)
|
|
1932
1932
|
width = svg.width.baseVal.value;
|
|
1933
1933
|
height = svg.height.baseVal.value;
|
|
1934
|
-
} catch (
|
|
1934
|
+
} catch (_e) {
|
|
1935
1935
|
// Fallback for relative units - use viewBox or rendered size
|
|
1936
1936
|
if (svg.viewBox && svg.viewBox.baseVal) {
|
|
1937
1937
|
width = svg.viewBox.baseVal.width;
|
|
@@ -1950,8 +1950,8 @@
|
|
|
1950
1950
|
// Apply aggressive downsizing for MathJax SVGs
|
|
1951
1951
|
let scaleFactor = 0.04; // Further reduced for smaller output
|
|
1952
1952
|
|
|
1953
|
-
|
|
1954
|
-
|
|
1953
|
+
const scaledWidth = width * scaleFactor;
|
|
1954
|
+
const scaledHeight = height * scaleFactor;
|
|
1955
1955
|
|
|
1956
1956
|
// If still too large after base scaling, scale down further
|
|
1957
1957
|
if (scaledWidth > targetMaxWidth || scaledHeight > targetMaxHeight) {
|
|
@@ -2184,7 +2184,7 @@
|
|
|
2184
2184
|
let mapDataUrl = '';
|
|
2185
2185
|
try {
|
|
2186
2186
|
mapDataUrl = canvas.toDataURL('image/png', 1.0);
|
|
2187
|
-
} catch (
|
|
2187
|
+
} catch (_e) {
|
|
2188
2188
|
console.warn('Map canvas tainted; falling back to placeholder');
|
|
2189
2189
|
}
|
|
2190
2190
|
|
|
@@ -2547,7 +2547,8 @@
|
|
|
2547
2547
|
const DEFAULT_OPTIONS = {
|
|
2548
2548
|
mode: 'split', // 'source' | 'preview' | 'split'
|
|
2549
2549
|
showToolbar: true,
|
|
2550
|
-
showRemoveHR: false, // Show button to remove horizontal rules (---)
|
|
2550
|
+
showRemoveHR: false, // Show button to remove horizontal rules (---)
|
|
2551
|
+
showLazyLinefeeds: false, // Show button to convert lazy linefeeds
|
|
2551
2552
|
theme: 'auto', // 'light' | 'dark' | 'auto'
|
|
2552
2553
|
lazy_linefeeds: false,
|
|
2553
2554
|
inline_styles: false, // Use CSS classes (false) or inline styles (true)
|
|
@@ -2558,7 +2559,9 @@
|
|
|
2558
2559
|
mermaid: false
|
|
2559
2560
|
},
|
|
2560
2561
|
customFences: {}, // { 'language': (code, lang) => html }
|
|
2561
|
-
enableComplexFences: true // Enable CSV tables, math rendering, SVG, etc.
|
|
2562
|
+
enableComplexFences: true, // Enable CSV tables, math rendering, SVG, etc.
|
|
2563
|
+
showUndoRedo: false, // Show undo/redo toolbar buttons
|
|
2564
|
+
undoStackSize: 100 // Maximum number of undo states to keep
|
|
2562
2565
|
};
|
|
2563
2566
|
|
|
2564
2567
|
/**
|
|
@@ -2583,6 +2586,11 @@
|
|
|
2583
2586
|
this._html = '';
|
|
2584
2587
|
this.currentMode = this.options.mode;
|
|
2585
2588
|
this.updateTimer = null;
|
|
2589
|
+
|
|
2590
|
+
// Undo/redo state
|
|
2591
|
+
this._undoStack = [];
|
|
2592
|
+
this._redoStack = [];
|
|
2593
|
+
this._isUndoRedo = false;
|
|
2586
2594
|
|
|
2587
2595
|
// Initialize
|
|
2588
2596
|
this.initPromise = this.init();
|
|
@@ -2675,6 +2683,23 @@
|
|
|
2675
2683
|
toolbar.appendChild(btn);
|
|
2676
2684
|
});
|
|
2677
2685
|
|
|
2686
|
+
// Undo/Redo buttons (if enabled)
|
|
2687
|
+
if (this.options.showUndoRedo) {
|
|
2688
|
+
const undoBtn = document.createElement('button');
|
|
2689
|
+
undoBtn.className = 'qde-btn disabled';
|
|
2690
|
+
undoBtn.dataset.action = 'undo';
|
|
2691
|
+
undoBtn.textContent = 'Undo';
|
|
2692
|
+
undoBtn.title = 'Undo (Ctrl+Z)';
|
|
2693
|
+
toolbar.appendChild(undoBtn);
|
|
2694
|
+
|
|
2695
|
+
const redoBtn = document.createElement('button');
|
|
2696
|
+
redoBtn.className = 'qde-btn disabled';
|
|
2697
|
+
redoBtn.dataset.action = 'redo';
|
|
2698
|
+
redoBtn.textContent = 'Redo';
|
|
2699
|
+
redoBtn.title = 'Redo (Ctrl+Shift+Z / Ctrl+Y)';
|
|
2700
|
+
toolbar.appendChild(redoBtn);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2678
2703
|
// Spacer
|
|
2679
2704
|
const spacer = document.createElement('span');
|
|
2680
2705
|
spacer.className = 'qde-spacer';
|
|
@@ -2705,6 +2730,16 @@
|
|
|
2705
2730
|
removeHRBtn.title = 'Remove all horizontal rules (---) from markdown';
|
|
2706
2731
|
toolbar.appendChild(removeHRBtn);
|
|
2707
2732
|
}
|
|
2733
|
+
|
|
2734
|
+
// Lazy linefeeds button (if enabled)
|
|
2735
|
+
if (this.options.showLazyLinefeeds) {
|
|
2736
|
+
const lazyLFBtn = document.createElement('button');
|
|
2737
|
+
lazyLFBtn.className = 'qde-btn';
|
|
2738
|
+
lazyLFBtn.dataset.action = 'lazy-linefeeds';
|
|
2739
|
+
lazyLFBtn.textContent = 'Fix Linefeeds';
|
|
2740
|
+
lazyLFBtn.title = 'Convert single newlines to paragraph breaks (one-time transform)';
|
|
2741
|
+
toolbar.appendChild(lazyLFBtn);
|
|
2742
|
+
}
|
|
2708
2743
|
|
|
2709
2744
|
return toolbar;
|
|
2710
2745
|
}
|
|
@@ -2757,6 +2792,11 @@
|
|
|
2757
2792
|
color: white;
|
|
2758
2793
|
border-color: #0056b3;
|
|
2759
2794
|
}
|
|
2795
|
+
|
|
2796
|
+
.qde-btn.disabled {
|
|
2797
|
+
opacity: 0.4;
|
|
2798
|
+
pointer-events: none;
|
|
2799
|
+
}
|
|
2760
2800
|
|
|
2761
2801
|
.qde-spacer {
|
|
2762
2802
|
flex: 1;
|
|
@@ -3041,6 +3081,21 @@
|
|
|
3041
3081
|
e.preventDefault();
|
|
3042
3082
|
this.setMode('preview');
|
|
3043
3083
|
break;
|
|
3084
|
+
case 'z':
|
|
3085
|
+
case 'Z':
|
|
3086
|
+
if (e.shiftKey) {
|
|
3087
|
+
e.preventDefault();
|
|
3088
|
+
this.redo();
|
|
3089
|
+
} else {
|
|
3090
|
+
e.preventDefault();
|
|
3091
|
+
this.undo();
|
|
3092
|
+
}
|
|
3093
|
+
break;
|
|
3094
|
+
case 'y':
|
|
3095
|
+
case 'Y':
|
|
3096
|
+
e.preventDefault();
|
|
3097
|
+
this.redo();
|
|
3098
|
+
break;
|
|
3044
3099
|
}
|
|
3045
3100
|
}
|
|
3046
3101
|
});
|
|
@@ -3070,6 +3125,12 @@
|
|
|
3070
3125
|
* Update from markdown source
|
|
3071
3126
|
*/
|
|
3072
3127
|
updateFromMarkdown(markdown) {
|
|
3128
|
+
// Push current state to undo stack before changing (unless this is an undo/redo operation)
|
|
3129
|
+
if (!this._isUndoRedo) {
|
|
3130
|
+
this._pushUndoState(markdown || '');
|
|
3131
|
+
}
|
|
3132
|
+
this._isUndoRedo = false;
|
|
3133
|
+
|
|
3073
3134
|
this._markdown = markdown || '';
|
|
3074
3135
|
|
|
3075
3136
|
// Show placeholder if empty
|
|
@@ -3095,16 +3156,9 @@
|
|
|
3095
3156
|
if (window.MathJax && window.MathJax.typesetPromise) {
|
|
3096
3157
|
const mathElements = this.previewPanel.querySelectorAll('.math-display');
|
|
3097
3158
|
if (mathElements.length > 0) {
|
|
3098
|
-
mathElements.forEach(el => {
|
|
3099
|
-
});
|
|
3100
3159
|
window.MathJax.typesetPromise(Array.from(mathElements))
|
|
3101
|
-
.
|
|
3102
|
-
|
|
3103
|
-
el.querySelector('mjx-container');
|
|
3104
|
-
});
|
|
3105
|
-
})
|
|
3106
|
-
.catch(err => {
|
|
3107
|
-
console.warn('MathJax batch processing failed:', err);
|
|
3160
|
+
.catch(_err => {
|
|
3161
|
+
console.warn('MathJax batch processing failed:', _err);
|
|
3108
3162
|
});
|
|
3109
3163
|
}
|
|
3110
3164
|
}
|
|
@@ -3341,7 +3395,7 @@
|
|
|
3341
3395
|
// Remove event handlers
|
|
3342
3396
|
const walker = document.createTreeWalker(svg, NodeFilter.SHOW_ELEMENT);
|
|
3343
3397
|
let node;
|
|
3344
|
-
while (node = walker.nextNode()) {
|
|
3398
|
+
while ((node = walker.nextNode())) {
|
|
3345
3399
|
for (let i = node.attributes.length - 1; i >= 0; i--) {
|
|
3346
3400
|
const attr = node.attributes[i];
|
|
3347
3401
|
if (attr.name.startsWith('on') || attr.value.includes('javascript:')) {
|
|
@@ -3431,7 +3485,7 @@
|
|
|
3431
3485
|
/**
|
|
3432
3486
|
* Render math with MathJax (SVG output for better copy support)
|
|
3433
3487
|
*/
|
|
3434
|
-
renderMath(code,
|
|
3488
|
+
renderMath(code, _lang) {
|
|
3435
3489
|
const id = `math-${Math.random().toString(36).substring(2, 15)}`;
|
|
3436
3490
|
|
|
3437
3491
|
// Create container exactly like squibview
|
|
@@ -3554,11 +3608,11 @@
|
|
|
3554
3608
|
|
|
3555
3609
|
html += '</table>';
|
|
3556
3610
|
return html;
|
|
3557
|
-
} catch (
|
|
3611
|
+
} catch (_err) {
|
|
3558
3612
|
return `<pre data-qd-fence="\`\`\`" data-qd-lang="${lang}" data-qd-source="${escapedCode}">${escapedCode}</pre>`;
|
|
3559
3613
|
}
|
|
3560
3614
|
}
|
|
3561
|
-
|
|
3615
|
+
|
|
3562
3616
|
/**
|
|
3563
3617
|
* Parse CSV line handling quoted values
|
|
3564
3618
|
*/
|
|
@@ -3602,13 +3656,13 @@
|
|
|
3602
3656
|
try {
|
|
3603
3657
|
const data = JSON.parse(code);
|
|
3604
3658
|
toHighlight = JSON.stringify(data, null, 2);
|
|
3605
|
-
} catch (
|
|
3659
|
+
} catch (_e) {
|
|
3606
3660
|
// Use original if not valid JSON
|
|
3607
3661
|
}
|
|
3608
3662
|
|
|
3609
3663
|
const highlighted = hljs.highlight(toHighlight, { language: 'json' }).value;
|
|
3610
3664
|
return `<pre class="qde-json" data-qd-fence="\`\`\`" data-qd-lang="${lang}"><code class="hljs language-json">${highlighted}</code></pre>`;
|
|
3611
|
-
} catch (
|
|
3665
|
+
} catch (_e) {
|
|
3612
3666
|
// Fall through if highlighting fails
|
|
3613
3667
|
}
|
|
3614
3668
|
}
|
|
@@ -3701,7 +3755,7 @@
|
|
|
3701
3755
|
if (loaded) {
|
|
3702
3756
|
renderMap();
|
|
3703
3757
|
} else {
|
|
3704
|
-
const element = document.getElementById(
|
|
3758
|
+
const element = document.getElementById(mapId + '-container');
|
|
3705
3759
|
if (element) {
|
|
3706
3760
|
element.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">Failed to load map library</div>';
|
|
3707
3761
|
}
|
|
@@ -3988,24 +4042,46 @@
|
|
|
3988
4042
|
}
|
|
3989
4043
|
|
|
3990
4044
|
/**
|
|
3991
|
-
* Apply theme
|
|
4045
|
+
* Apply the current theme (based on this.options.theme)
|
|
3992
4046
|
*/
|
|
3993
4047
|
applyTheme() {
|
|
3994
4048
|
const theme = this.options.theme;
|
|
3995
|
-
|
|
4049
|
+
|
|
4050
|
+
// Tear down any previous auto-mode listener so we don't stack them
|
|
4051
|
+
if (this._autoThemeListener) {
|
|
4052
|
+
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this._autoThemeListener);
|
|
4053
|
+
this._autoThemeListener = null;
|
|
4054
|
+
}
|
|
4055
|
+
|
|
3996
4056
|
if (theme === 'auto') {
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
this.
|
|
4000
|
-
|
|
4001
|
-
// Listen for changes
|
|
4002
|
-
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
4057
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
4058
|
+
this.container.classList.toggle('qde-dark', mq.matches);
|
|
4059
|
+
this._autoThemeListener = (e) => {
|
|
4003
4060
|
this.container.classList.toggle('qde-dark', e.matches);
|
|
4004
|
-
}
|
|
4061
|
+
};
|
|
4062
|
+
mq.addEventListener('change', this._autoThemeListener);
|
|
4005
4063
|
} else {
|
|
4006
4064
|
this.container.classList.toggle('qde-dark', theme === 'dark');
|
|
4007
4065
|
}
|
|
4008
4066
|
}
|
|
4067
|
+
|
|
4068
|
+
/**
|
|
4069
|
+
* Set theme at runtime. Accepts 'light', 'dark', or 'auto'.
|
|
4070
|
+
* @param {'light'|'dark'|'auto'} theme
|
|
4071
|
+
*/
|
|
4072
|
+
setTheme(theme) {
|
|
4073
|
+
if (!['light', 'dark', 'auto'].includes(theme)) return;
|
|
4074
|
+
this.options.theme = theme;
|
|
4075
|
+
this.applyTheme();
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
/**
|
|
4079
|
+
* Get the current theme option (as configured, not resolved).
|
|
4080
|
+
* @returns {'light'|'dark'|'auto'}
|
|
4081
|
+
*/
|
|
4082
|
+
getTheme() {
|
|
4083
|
+
return this.options.theme;
|
|
4084
|
+
}
|
|
4009
4085
|
|
|
4010
4086
|
/**
|
|
4011
4087
|
* Set lazy linefeeds option
|
|
@@ -4075,6 +4151,105 @@
|
|
|
4075
4151
|
}
|
|
4076
4152
|
}
|
|
4077
4153
|
|
|
4154
|
+
// --- Undo / Redo ---
|
|
4155
|
+
|
|
4156
|
+
/**
|
|
4157
|
+
* Push current markdown state onto the undo stack (called before a change).
|
|
4158
|
+
* Only pushes if the new state differs from the current state.
|
|
4159
|
+
* @param {string} newMarkdown - the incoming markdown (used to detect no-op)
|
|
4160
|
+
* @private
|
|
4161
|
+
*/
|
|
4162
|
+
_pushUndoState(newMarkdown) {
|
|
4163
|
+
// Don't push if the content hasn't actually changed
|
|
4164
|
+
if (newMarkdown === this._markdown) return;
|
|
4165
|
+
|
|
4166
|
+
this._undoStack.push(this._markdown);
|
|
4167
|
+
|
|
4168
|
+
// Enforce max stack size
|
|
4169
|
+
const max = this.options.undoStackSize || 100;
|
|
4170
|
+
if (this._undoStack.length > max) {
|
|
4171
|
+
this._undoStack.splice(0, this._undoStack.length - max);
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4174
|
+
// Any new edit clears the redo stack
|
|
4175
|
+
this._redoStack = [];
|
|
4176
|
+
this._updateUndoButtons();
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4179
|
+
/**
|
|
4180
|
+
* Undo the last change. Restores the previous markdown state.
|
|
4181
|
+
*/
|
|
4182
|
+
undo() {
|
|
4183
|
+
if (!this.canUndo()) return;
|
|
4184
|
+
// Save current state to redo stack
|
|
4185
|
+
this._redoStack.push(this._markdown);
|
|
4186
|
+
const previous = this._undoStack.pop();
|
|
4187
|
+
this._isUndoRedo = true;
|
|
4188
|
+
// Update state directly (setMarkdown is async; keep it synchronous here)
|
|
4189
|
+
this._markdown = previous;
|
|
4190
|
+
if (this.sourceTextarea) {
|
|
4191
|
+
this.sourceTextarea.value = previous;
|
|
4192
|
+
}
|
|
4193
|
+
this.updateFromMarkdown(previous);
|
|
4194
|
+
this._updateUndoButtons();
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
/**
|
|
4198
|
+
* Redo the last undone change.
|
|
4199
|
+
*/
|
|
4200
|
+
redo() {
|
|
4201
|
+
if (!this.canRedo()) return;
|
|
4202
|
+
// Save current state to undo stack
|
|
4203
|
+
this._undoStack.push(this._markdown);
|
|
4204
|
+
const next = this._redoStack.pop();
|
|
4205
|
+
this._isUndoRedo = true;
|
|
4206
|
+
this._markdown = next;
|
|
4207
|
+
if (this.sourceTextarea) {
|
|
4208
|
+
this.sourceTextarea.value = next;
|
|
4209
|
+
}
|
|
4210
|
+
this.updateFromMarkdown(next);
|
|
4211
|
+
this._updateUndoButtons();
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
/**
|
|
4215
|
+
* @returns {boolean} true if undo is possible
|
|
4216
|
+
*/
|
|
4217
|
+
canUndo() {
|
|
4218
|
+
return this._undoStack.length > 0;
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
/**
|
|
4222
|
+
* @returns {boolean} true if redo is possible
|
|
4223
|
+
*/
|
|
4224
|
+
canRedo() {
|
|
4225
|
+
return this._redoStack.length > 0;
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
/**
|
|
4229
|
+
* Clear the undo and redo history.
|
|
4230
|
+
*/
|
|
4231
|
+
clearHistory() {
|
|
4232
|
+
this._undoStack = [];
|
|
4233
|
+
this._redoStack = [];
|
|
4234
|
+
this._updateUndoButtons();
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
/**
|
|
4238
|
+
* Update the disabled state of the undo/redo toolbar buttons.
|
|
4239
|
+
* @private
|
|
4240
|
+
*/
|
|
4241
|
+
_updateUndoButtons() {
|
|
4242
|
+
if (!this.toolbar) return;
|
|
4243
|
+
const undoBtn = this.toolbar.querySelector('[data-action="undo"]');
|
|
4244
|
+
const redoBtn = this.toolbar.querySelector('[data-action="redo"]');
|
|
4245
|
+
if (undoBtn) {
|
|
4246
|
+
undoBtn.classList.toggle('disabled', !this.canUndo());
|
|
4247
|
+
}
|
|
4248
|
+
if (redoBtn) {
|
|
4249
|
+
redoBtn.classList.toggle('disabled', !this.canRedo());
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4078
4253
|
/**
|
|
4079
4254
|
* Handle toolbar actions
|
|
4080
4255
|
*/
|
|
@@ -4092,6 +4267,15 @@
|
|
|
4092
4267
|
case 'remove-hr':
|
|
4093
4268
|
this.removeHR();
|
|
4094
4269
|
break;
|
|
4270
|
+
case 'lazy-linefeeds':
|
|
4271
|
+
this.convertLazyLinefeeds();
|
|
4272
|
+
break;
|
|
4273
|
+
case 'undo':
|
|
4274
|
+
this.undo();
|
|
4275
|
+
break;
|
|
4276
|
+
case 'redo':
|
|
4277
|
+
this.redo();
|
|
4278
|
+
break;
|
|
4095
4279
|
}
|
|
4096
4280
|
}
|
|
4097
4281
|
|
|
@@ -4179,24 +4363,13 @@
|
|
|
4179
4363
|
}
|
|
4180
4364
|
|
|
4181
4365
|
/**
|
|
4182
|
-
* Remove all horizontal rules (---) from markdown
|
|
4366
|
+
* Remove all horizontal rules (---) from markdown source.
|
|
4367
|
+
* Preserves content inside fences (``` or ~~~) and table separator rows.
|
|
4183
4368
|
*/
|
|
4184
4369
|
async removeHR() {
|
|
4185
|
-
|
|
4186
|
-
// Matches: ---, ___, ***, ----, etc. with optional spaces
|
|
4187
|
-
const cleaned = this._markdown
|
|
4188
|
-
.split('\n')
|
|
4189
|
-
.filter(line => {
|
|
4190
|
-
// Keep lines that aren't just HR patterns
|
|
4191
|
-
const trimmed = line.trim();
|
|
4192
|
-
// Match HR patterns: 3+ of -, _, or * with optional spaces between
|
|
4193
|
-
return !(/^[-_*](\s*[-_*]){2,}\s*$/.test(trimmed));
|
|
4194
|
-
})
|
|
4195
|
-
.join('\n');
|
|
4196
|
-
|
|
4197
|
-
// Update the markdown
|
|
4370
|
+
const cleaned = QuikdownEditor.removeHRFromMarkdown(this._markdown);
|
|
4198
4371
|
await this.setMarkdown(cleaned);
|
|
4199
|
-
|
|
4372
|
+
|
|
4200
4373
|
// Visual feedback if toolbar button exists
|
|
4201
4374
|
const btn = this.toolbar?.querySelector('[data-action="remove-hr"]');
|
|
4202
4375
|
if (btn) {
|
|
@@ -4207,6 +4380,202 @@
|
|
|
4207
4380
|
}, 1500);
|
|
4208
4381
|
}
|
|
4209
4382
|
}
|
|
4383
|
+
|
|
4384
|
+
/**
|
|
4385
|
+
* Static: remove horizontal rules from markdown string.
|
|
4386
|
+
* Safe for fences, tables, and all markdown constructs.
|
|
4387
|
+
* Can be used headless without an editor instance.
|
|
4388
|
+
* @param {string} markdown - source markdown
|
|
4389
|
+
* @returns {string} markdown with standalone HRs removed
|
|
4390
|
+
*/
|
|
4391
|
+
static removeHRFromMarkdown(markdown) {
|
|
4392
|
+
const lines = (markdown || '').split('\n');
|
|
4393
|
+
const result = [];
|
|
4394
|
+
let inFence = false;
|
|
4395
|
+
let fenceChar = null; // '`' or '~'
|
|
4396
|
+
let fenceLen = 0; // length of opening fence marker
|
|
4397
|
+
|
|
4398
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4399
|
+
const line = lines[i];
|
|
4400
|
+
const trimmed = line.trim();
|
|
4401
|
+
|
|
4402
|
+
// Track fence open/close (``` or ~~~, 3+ chars)
|
|
4403
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
4404
|
+
if (fenceMatch) {
|
|
4405
|
+
const matchChar = fenceMatch[1][0];
|
|
4406
|
+
const matchLen = fenceMatch[1].length;
|
|
4407
|
+
if (!inFence) {
|
|
4408
|
+
inFence = true;
|
|
4409
|
+
fenceChar = matchChar;
|
|
4410
|
+
fenceLen = matchLen;
|
|
4411
|
+
result.push(line);
|
|
4412
|
+
continue;
|
|
4413
|
+
} else if (matchChar === fenceChar && matchLen >= fenceLen && /^(`{3,}|~{3,})\s*$/.test(trimmed)) {
|
|
4414
|
+
// Closing fence: same char, at least as many chars, no trailing content
|
|
4415
|
+
inFence = false;
|
|
4416
|
+
fenceChar = null;
|
|
4417
|
+
fenceLen = 0;
|
|
4418
|
+
result.push(line);
|
|
4419
|
+
continue;
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
// Inside a fence — keep everything
|
|
4424
|
+
if (inFence) {
|
|
4425
|
+
result.push(line);
|
|
4426
|
+
continue;
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
// Detect table row/separator with pipes — always keep
|
|
4430
|
+
if (/^\|.*\|$/.test(trimmed) || (/^[-| :]+$/.test(trimmed) && trimmed.includes('|'))) {
|
|
4431
|
+
result.push(line);
|
|
4432
|
+
continue;
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
// Check if this line is a standalone HR
|
|
4436
|
+
const isHR = /^[-_*](\s*[-_*]){2,}\s*$/.test(trimmed);
|
|
4437
|
+
if (isHR) {
|
|
4438
|
+
// Table separator heuristic: immediately adjacent lines (no blank
|
|
4439
|
+
// lines between) that look like table rows protect this HR-like line
|
|
4440
|
+
const prevLine = i > 0 ? lines[i - 1].trim() : '';
|
|
4441
|
+
const nextLine = i < lines.length - 1 ? lines[i + 1].trim() : '';
|
|
4442
|
+
if (_looksLikeTableRow(prevLine) || _looksLikeTableRow(nextLine)) {
|
|
4443
|
+
result.push(line);
|
|
4444
|
+
continue;
|
|
4445
|
+
}
|
|
4446
|
+
// It's a real HR — skip it
|
|
4447
|
+
continue;
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
result.push(line);
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4453
|
+
return result.join('\n');
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
/**
|
|
4457
|
+
* Convert lazy linefeeds in markdown source.
|
|
4458
|
+
* Replaces single newlines with double newlines (adds real line breaks)
|
|
4459
|
+
* except inside fences, tables, and other block-level constructs.
|
|
4460
|
+
* Idempotent: calling multiple times produces the same result.
|
|
4461
|
+
* Can be used as a toolbar action or headless via the static method.
|
|
4462
|
+
*/
|
|
4463
|
+
async convertLazyLinefeeds() {
|
|
4464
|
+
const converted = QuikdownEditor.convertLazyLinefeeds(this._markdown);
|
|
4465
|
+
await this.setMarkdown(converted);
|
|
4466
|
+
|
|
4467
|
+
// Visual feedback if toolbar button exists
|
|
4468
|
+
const btn = this.toolbar?.querySelector('[data-action="lazy-linefeeds"]');
|
|
4469
|
+
if (btn) {
|
|
4470
|
+
const originalText = btn.textContent;
|
|
4471
|
+
btn.textContent = 'Converted!';
|
|
4472
|
+
setTimeout(() => {
|
|
4473
|
+
btn.textContent = originalText;
|
|
4474
|
+
}, 1500);
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
/**
|
|
4479
|
+
* Static: convert lazy linefeeds in markdown source.
|
|
4480
|
+
* Turns single \n between non-blank lines into \n\n so each line becomes
|
|
4481
|
+
* its own paragraph / hard break. Idempotent — already-doubled newlines
|
|
4482
|
+
* are not doubled again. Fences, tables, lists, blockquotes, headings,
|
|
4483
|
+
* and HTML blocks are left untouched.
|
|
4484
|
+
* @param {string} markdown - source markdown
|
|
4485
|
+
* @returns {string} markdown with lazy linefeeds resolved
|
|
4486
|
+
*/
|
|
4487
|
+
static convertLazyLinefeeds(markdown) {
|
|
4488
|
+
const lines = (markdown || '').split('\n');
|
|
4489
|
+
const result = [];
|
|
4490
|
+
let inFence = false;
|
|
4491
|
+
let fenceChar = null;
|
|
4492
|
+
let fenceLen = 0;
|
|
4493
|
+
let inHTMLBlock = false;
|
|
4494
|
+
|
|
4495
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4496
|
+
const line = lines[i];
|
|
4497
|
+
const trimmed = line.trim();
|
|
4498
|
+
|
|
4499
|
+
// Track fence open/close
|
|
4500
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
4501
|
+
if (fenceMatch) {
|
|
4502
|
+
const matchChar = fenceMatch[1][0];
|
|
4503
|
+
const matchLen = fenceMatch[1].length;
|
|
4504
|
+
if (!inFence) {
|
|
4505
|
+
inFence = true;
|
|
4506
|
+
fenceChar = matchChar;
|
|
4507
|
+
fenceLen = matchLen;
|
|
4508
|
+
result.push(line);
|
|
4509
|
+
continue;
|
|
4510
|
+
} else if (matchChar === fenceChar && matchLen >= fenceLen && /^(`{3,}|~{3,})\s*$/.test(trimmed)) {
|
|
4511
|
+
inFence = false;
|
|
4512
|
+
fenceChar = null;
|
|
4513
|
+
fenceLen = 0;
|
|
4514
|
+
result.push(line);
|
|
4515
|
+
continue;
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4519
|
+
// Inside fence — pass through
|
|
4520
|
+
if (inFence) {
|
|
4521
|
+
result.push(line);
|
|
4522
|
+
continue;
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
// Track HTML blocks (lines starting with < and ending with >)
|
|
4526
|
+
if (/^<[a-zA-Z]/.test(trimmed)) inHTMLBlock = true;
|
|
4527
|
+
if (inHTMLBlock) {
|
|
4528
|
+
result.push(line);
|
|
4529
|
+
if (/>$/.test(trimmed) || trimmed === '') inHTMLBlock = false;
|
|
4530
|
+
continue;
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
// Always pass through blank lines, but never add extras
|
|
4534
|
+
if (trimmed === '') {
|
|
4535
|
+
// Avoid doubling: don't add blank line if the last result line is already blank
|
|
4536
|
+
if (result.length === 0 || result[result.length - 1].trim() !== '') {
|
|
4537
|
+
result.push(line);
|
|
4538
|
+
}
|
|
4539
|
+
continue;
|
|
4540
|
+
}
|
|
4541
|
+
|
|
4542
|
+
// Skip conversion for block-level constructs
|
|
4543
|
+
const isBlockElement = (
|
|
4544
|
+
/^#{1,6}\s/.test(trimmed) || // headings
|
|
4545
|
+
/^[-_*](\s*[-_*]){2,}\s*$/.test(trimmed) || // horizontal rules
|
|
4546
|
+
/^(\d+\.|-|\*|\+)\s/.test(trimmed) || // list items
|
|
4547
|
+
/^>/.test(trimmed) || // blockquotes
|
|
4548
|
+
/^\|/.test(trimmed) // table rows
|
|
4549
|
+
);
|
|
4550
|
+
|
|
4551
|
+
if (isBlockElement) {
|
|
4552
|
+
result.push(line);
|
|
4553
|
+
continue;
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
// For plain paragraph text: if previous result line is non-blank
|
|
4557
|
+
// plain text, insert a blank line between them (making the single
|
|
4558
|
+
// newline into a paragraph break). This is the lazy→strict conversion.
|
|
4559
|
+
if (result.length > 0) {
|
|
4560
|
+
const prevLine = result[result.length - 1];
|
|
4561
|
+
const prevTrimmed = prevLine.trim();
|
|
4562
|
+
// Only insert blank line if prev is non-blank, non-block text
|
|
4563
|
+
if (prevTrimmed !== '' &&
|
|
4564
|
+
!/^#{1,6}\s/.test(prevTrimmed) &&
|
|
4565
|
+
!/^[-_*](\s*[-_*]){2,}\s*$/.test(prevTrimmed) &&
|
|
4566
|
+
!/^(\d+\.|-|\*|\+)\s/.test(prevTrimmed) &&
|
|
4567
|
+
!/^>/.test(prevTrimmed) &&
|
|
4568
|
+
!/^\|/.test(prevTrimmed) &&
|
|
4569
|
+
!/^(`{3,}|~{3,})/.test(prevTrimmed)) {
|
|
4570
|
+
result.push('');
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
result.push(line);
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4577
|
+
return result.join('\n');
|
|
4578
|
+
}
|
|
4210
4579
|
|
|
4211
4580
|
/**
|
|
4212
4581
|
* Copy rendered content as rich text
|
|
@@ -4250,6 +4619,13 @@
|
|
|
4250
4619
|
}
|
|
4251
4620
|
}
|
|
4252
4621
|
|
|
4622
|
+
// --- Internal helpers for removeHR fence/table awareness ---
|
|
4623
|
+
|
|
4624
|
+
/** Heuristic: does this line look like a markdown table row? */
|
|
4625
|
+
function _looksLikeTableRow(line) {
|
|
4626
|
+
return line.includes('|');
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4253
4629
|
// Export for CommonJS (needed for bundled ESM to work with Jest)
|
|
4254
4630
|
if (typeof module !== 'undefined' && module.exports) {
|
|
4255
4631
|
module.exports = QuikdownEditor;
|