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
|
*/
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
// Version will be injected at build time
|
|
21
|
-
const quikdownVersion = '1.
|
|
21
|
+
const quikdownVersion = '1.2.2';
|
|
22
22
|
|
|
23
23
|
// Constants for reuse
|
|
24
24
|
const CLASS_PREFIX = 'quikdown-';
|
|
@@ -268,7 +268,7 @@ function quikdown(markdown, options = {}) {
|
|
|
268
268
|
html = '<p>' + html + '</p>';
|
|
269
269
|
} else {
|
|
270
270
|
// Standard: two spaces at end of line for line breaks
|
|
271
|
-
html = html.replace(/
|
|
271
|
+
html = html.replace(/ {2}$/gm, `<br${getAttr('br')}>`);
|
|
272
272
|
|
|
273
273
|
// Paragraphs (double newlines)
|
|
274
274
|
// Don't add </p> after block elements (they're not in paragraphs)
|
|
@@ -297,7 +297,7 @@ function quikdown(markdown, options = {}) {
|
|
|
297
297
|
[/(<\/table>)<\/p>/g, '$1'],
|
|
298
298
|
[/<p>(<pre[^>]*>)/g, '$1'],
|
|
299
299
|
[/(<\/pre>)<\/p>/g, '$1'],
|
|
300
|
-
[new RegExp(`<p>(${PLACEHOLDER_CB}\\d+§)
|
|
300
|
+
[new RegExp(`<p>(${PLACEHOLDER_CB}\\d+§)</p>`, 'g'), '$1']
|
|
301
301
|
];
|
|
302
302
|
|
|
303
303
|
cleanupPatterns.forEach(([pattern, replacement]) => {
|
|
@@ -503,7 +503,7 @@ function processLists(text, getAttr, inline_styles, bidirectional) {
|
|
|
503
503
|
|
|
504
504
|
const lines = text.split('\n');
|
|
505
505
|
const result = [];
|
|
506
|
-
|
|
506
|
+
const listStack = []; // Track nested lists
|
|
507
507
|
|
|
508
508
|
// Helper to escape HTML for data-qd attributes
|
|
509
509
|
const escapeHtml = (text) => text.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]);
|
|
@@ -713,7 +713,7 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
713
713
|
|
|
714
714
|
// Process children with context
|
|
715
715
|
let childContent = '';
|
|
716
|
-
for (
|
|
716
|
+
for (const child of node.childNodes) {
|
|
717
717
|
childContent += walkNode(child, { parentTag: tag, ...parentContext });
|
|
718
718
|
}
|
|
719
719
|
|
|
@@ -947,7 +947,7 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
947
947
|
let index = 1;
|
|
948
948
|
const indent = ' '.repeat(depth);
|
|
949
949
|
|
|
950
|
-
for (
|
|
950
|
+
for (const child of listNode.children) {
|
|
951
951
|
if (child.tagName !== 'LI') continue;
|
|
952
952
|
|
|
953
953
|
const dataQd = child.getAttribute('data-qd');
|
|
@@ -960,7 +960,7 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
960
960
|
marker = '-';
|
|
961
961
|
// Get text without the checkbox
|
|
962
962
|
let text = '';
|
|
963
|
-
for (
|
|
963
|
+
for (const node of child.childNodes) {
|
|
964
964
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
965
965
|
text += node.textContent;
|
|
966
966
|
} else if (node.tagName && node.tagName !== 'INPUT') {
|
|
@@ -971,7 +971,7 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
971
971
|
} else {
|
|
972
972
|
let itemContent = '';
|
|
973
973
|
|
|
974
|
-
for (
|
|
974
|
+
for (const node of child.childNodes) {
|
|
975
975
|
if (node.tagName === 'UL' || node.tagName === 'OL') {
|
|
976
976
|
itemContent += walkList(node, node.tagName === 'OL', depth + 1);
|
|
977
977
|
} else {
|
|
@@ -1000,7 +1000,7 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
1000
1000
|
const headerRow = thead.querySelector('tr');
|
|
1001
1001
|
if (headerRow) {
|
|
1002
1002
|
const headers = [];
|
|
1003
|
-
for (
|
|
1003
|
+
for (const th of headerRow.querySelectorAll('th')) {
|
|
1004
1004
|
headers.push(th.textContent.trim());
|
|
1005
1005
|
}
|
|
1006
1006
|
result += '| ' + headers.join(' | ') + ' |\n';
|
|
@@ -1019,9 +1019,9 @@ quikdown_bd.toMarkdown = function(htmlOrElement, options = {}) {
|
|
|
1019
1019
|
// Process body
|
|
1020
1020
|
const tbody = table.querySelector('tbody');
|
|
1021
1021
|
if (tbody) {
|
|
1022
|
-
for (
|
|
1022
|
+
for (const row of tbody.querySelectorAll('tr')) {
|
|
1023
1023
|
const cells = [];
|
|
1024
|
-
for (
|
|
1024
|
+
for (const td of row.querySelectorAll('td')) {
|
|
1025
1025
|
cells.push(td.textContent.trim());
|
|
1026
1026
|
}
|
|
1027
1027
|
if (cells.length > 0) {
|
|
@@ -1925,7 +1925,7 @@ async function getRenderedContent(previewPanel) {
|
|
|
1925
1925
|
// First try baseVal.value (works for absolute units)
|
|
1926
1926
|
width = svg.width.baseVal.value;
|
|
1927
1927
|
height = svg.height.baseVal.value;
|
|
1928
|
-
} catch (
|
|
1928
|
+
} catch (_e) {
|
|
1929
1929
|
// Fallback for relative units - use viewBox or rendered size
|
|
1930
1930
|
if (svg.viewBox && svg.viewBox.baseVal) {
|
|
1931
1931
|
width = svg.viewBox.baseVal.width;
|
|
@@ -1944,8 +1944,8 @@ async function getRenderedContent(previewPanel) {
|
|
|
1944
1944
|
// Apply aggressive downsizing for MathJax SVGs
|
|
1945
1945
|
let scaleFactor = 0.04; // Further reduced for smaller output
|
|
1946
1946
|
|
|
1947
|
-
|
|
1948
|
-
|
|
1947
|
+
const scaledWidth = width * scaleFactor;
|
|
1948
|
+
const scaledHeight = height * scaleFactor;
|
|
1949
1949
|
|
|
1950
1950
|
// If still too large after base scaling, scale down further
|
|
1951
1951
|
if (scaledWidth > targetMaxWidth || scaledHeight > targetMaxHeight) {
|
|
@@ -2178,7 +2178,7 @@ async function getRenderedContent(previewPanel) {
|
|
|
2178
2178
|
let mapDataUrl = '';
|
|
2179
2179
|
try {
|
|
2180
2180
|
mapDataUrl = canvas.toDataURL('image/png', 1.0);
|
|
2181
|
-
} catch (
|
|
2181
|
+
} catch (_e) {
|
|
2182
2182
|
console.warn('Map canvas tainted; falling back to placeholder');
|
|
2183
2183
|
}
|
|
2184
2184
|
|
|
@@ -2541,7 +2541,8 @@ async function getRenderedContent(previewPanel) {
|
|
|
2541
2541
|
const DEFAULT_OPTIONS = {
|
|
2542
2542
|
mode: 'split', // 'source' | 'preview' | 'split'
|
|
2543
2543
|
showToolbar: true,
|
|
2544
|
-
showRemoveHR: false, // Show button to remove horizontal rules (---)
|
|
2544
|
+
showRemoveHR: false, // Show button to remove horizontal rules (---)
|
|
2545
|
+
showLazyLinefeeds: false, // Show button to convert lazy linefeeds
|
|
2545
2546
|
theme: 'auto', // 'light' | 'dark' | 'auto'
|
|
2546
2547
|
lazy_linefeeds: false,
|
|
2547
2548
|
inline_styles: false, // Use CSS classes (false) or inline styles (true)
|
|
@@ -2552,7 +2553,9 @@ const DEFAULT_OPTIONS = {
|
|
|
2552
2553
|
mermaid: false
|
|
2553
2554
|
},
|
|
2554
2555
|
customFences: {}, // { 'language': (code, lang) => html }
|
|
2555
|
-
enableComplexFences: true // Enable CSV tables, math rendering, SVG, etc.
|
|
2556
|
+
enableComplexFences: true, // Enable CSV tables, math rendering, SVG, etc.
|
|
2557
|
+
showUndoRedo: false, // Show undo/redo toolbar buttons
|
|
2558
|
+
undoStackSize: 100 // Maximum number of undo states to keep
|
|
2556
2559
|
};
|
|
2557
2560
|
|
|
2558
2561
|
/**
|
|
@@ -2577,6 +2580,11 @@ class QuikdownEditor {
|
|
|
2577
2580
|
this._html = '';
|
|
2578
2581
|
this.currentMode = this.options.mode;
|
|
2579
2582
|
this.updateTimer = null;
|
|
2583
|
+
|
|
2584
|
+
// Undo/redo state
|
|
2585
|
+
this._undoStack = [];
|
|
2586
|
+
this._redoStack = [];
|
|
2587
|
+
this._isUndoRedo = false;
|
|
2580
2588
|
|
|
2581
2589
|
// Initialize
|
|
2582
2590
|
this.initPromise = this.init();
|
|
@@ -2669,6 +2677,23 @@ class QuikdownEditor {
|
|
|
2669
2677
|
toolbar.appendChild(btn);
|
|
2670
2678
|
});
|
|
2671
2679
|
|
|
2680
|
+
// Undo/Redo buttons (if enabled)
|
|
2681
|
+
if (this.options.showUndoRedo) {
|
|
2682
|
+
const undoBtn = document.createElement('button');
|
|
2683
|
+
undoBtn.className = 'qde-btn disabled';
|
|
2684
|
+
undoBtn.dataset.action = 'undo';
|
|
2685
|
+
undoBtn.textContent = 'Undo';
|
|
2686
|
+
undoBtn.title = 'Undo (Ctrl+Z)';
|
|
2687
|
+
toolbar.appendChild(undoBtn);
|
|
2688
|
+
|
|
2689
|
+
const redoBtn = document.createElement('button');
|
|
2690
|
+
redoBtn.className = 'qde-btn disabled';
|
|
2691
|
+
redoBtn.dataset.action = 'redo';
|
|
2692
|
+
redoBtn.textContent = 'Redo';
|
|
2693
|
+
redoBtn.title = 'Redo (Ctrl+Shift+Z / Ctrl+Y)';
|
|
2694
|
+
toolbar.appendChild(redoBtn);
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2672
2697
|
// Spacer
|
|
2673
2698
|
const spacer = document.createElement('span');
|
|
2674
2699
|
spacer.className = 'qde-spacer';
|
|
@@ -2699,6 +2724,16 @@ class QuikdownEditor {
|
|
|
2699
2724
|
removeHRBtn.title = 'Remove all horizontal rules (---) from markdown';
|
|
2700
2725
|
toolbar.appendChild(removeHRBtn);
|
|
2701
2726
|
}
|
|
2727
|
+
|
|
2728
|
+
// Lazy linefeeds button (if enabled)
|
|
2729
|
+
if (this.options.showLazyLinefeeds) {
|
|
2730
|
+
const lazyLFBtn = document.createElement('button');
|
|
2731
|
+
lazyLFBtn.className = 'qde-btn';
|
|
2732
|
+
lazyLFBtn.dataset.action = 'lazy-linefeeds';
|
|
2733
|
+
lazyLFBtn.textContent = 'Fix Linefeeds';
|
|
2734
|
+
lazyLFBtn.title = 'Convert single newlines to paragraph breaks (one-time transform)';
|
|
2735
|
+
toolbar.appendChild(lazyLFBtn);
|
|
2736
|
+
}
|
|
2702
2737
|
|
|
2703
2738
|
return toolbar;
|
|
2704
2739
|
}
|
|
@@ -2751,6 +2786,11 @@ class QuikdownEditor {
|
|
|
2751
2786
|
color: white;
|
|
2752
2787
|
border-color: #0056b3;
|
|
2753
2788
|
}
|
|
2789
|
+
|
|
2790
|
+
.qde-btn.disabled {
|
|
2791
|
+
opacity: 0.4;
|
|
2792
|
+
pointer-events: none;
|
|
2793
|
+
}
|
|
2754
2794
|
|
|
2755
2795
|
.qde-spacer {
|
|
2756
2796
|
flex: 1;
|
|
@@ -3035,6 +3075,21 @@ class QuikdownEditor {
|
|
|
3035
3075
|
e.preventDefault();
|
|
3036
3076
|
this.setMode('preview');
|
|
3037
3077
|
break;
|
|
3078
|
+
case 'z':
|
|
3079
|
+
case 'Z':
|
|
3080
|
+
if (e.shiftKey) {
|
|
3081
|
+
e.preventDefault();
|
|
3082
|
+
this.redo();
|
|
3083
|
+
} else {
|
|
3084
|
+
e.preventDefault();
|
|
3085
|
+
this.undo();
|
|
3086
|
+
}
|
|
3087
|
+
break;
|
|
3088
|
+
case 'y':
|
|
3089
|
+
case 'Y':
|
|
3090
|
+
e.preventDefault();
|
|
3091
|
+
this.redo();
|
|
3092
|
+
break;
|
|
3038
3093
|
}
|
|
3039
3094
|
}
|
|
3040
3095
|
});
|
|
@@ -3064,6 +3119,12 @@ class QuikdownEditor {
|
|
|
3064
3119
|
* Update from markdown source
|
|
3065
3120
|
*/
|
|
3066
3121
|
updateFromMarkdown(markdown) {
|
|
3122
|
+
// Push current state to undo stack before changing (unless this is an undo/redo operation)
|
|
3123
|
+
if (!this._isUndoRedo) {
|
|
3124
|
+
this._pushUndoState(markdown || '');
|
|
3125
|
+
}
|
|
3126
|
+
this._isUndoRedo = false;
|
|
3127
|
+
|
|
3067
3128
|
this._markdown = markdown || '';
|
|
3068
3129
|
|
|
3069
3130
|
// Show placeholder if empty
|
|
@@ -3089,16 +3150,9 @@ class QuikdownEditor {
|
|
|
3089
3150
|
if (window.MathJax && window.MathJax.typesetPromise) {
|
|
3090
3151
|
const mathElements = this.previewPanel.querySelectorAll('.math-display');
|
|
3091
3152
|
if (mathElements.length > 0) {
|
|
3092
|
-
mathElements.forEach(el => {
|
|
3093
|
-
});
|
|
3094
3153
|
window.MathJax.typesetPromise(Array.from(mathElements))
|
|
3095
|
-
.
|
|
3096
|
-
|
|
3097
|
-
el.querySelector('mjx-container');
|
|
3098
|
-
});
|
|
3099
|
-
})
|
|
3100
|
-
.catch(err => {
|
|
3101
|
-
console.warn('MathJax batch processing failed:', err);
|
|
3154
|
+
.catch(_err => {
|
|
3155
|
+
console.warn('MathJax batch processing failed:', _err);
|
|
3102
3156
|
});
|
|
3103
3157
|
}
|
|
3104
3158
|
}
|
|
@@ -3335,7 +3389,7 @@ class QuikdownEditor {
|
|
|
3335
3389
|
// Remove event handlers
|
|
3336
3390
|
const walker = document.createTreeWalker(svg, NodeFilter.SHOW_ELEMENT);
|
|
3337
3391
|
let node;
|
|
3338
|
-
while (node = walker.nextNode()) {
|
|
3392
|
+
while ((node = walker.nextNode())) {
|
|
3339
3393
|
for (let i = node.attributes.length - 1; i >= 0; i--) {
|
|
3340
3394
|
const attr = node.attributes[i];
|
|
3341
3395
|
if (attr.name.startsWith('on') || attr.value.includes('javascript:')) {
|
|
@@ -3425,7 +3479,7 @@ class QuikdownEditor {
|
|
|
3425
3479
|
/**
|
|
3426
3480
|
* Render math with MathJax (SVG output for better copy support)
|
|
3427
3481
|
*/
|
|
3428
|
-
renderMath(code,
|
|
3482
|
+
renderMath(code, _lang) {
|
|
3429
3483
|
const id = `math-${Math.random().toString(36).substring(2, 15)}`;
|
|
3430
3484
|
|
|
3431
3485
|
// Create container exactly like squibview
|
|
@@ -3548,11 +3602,11 @@ class QuikdownEditor {
|
|
|
3548
3602
|
|
|
3549
3603
|
html += '</table>';
|
|
3550
3604
|
return html;
|
|
3551
|
-
} catch (
|
|
3605
|
+
} catch (_err) {
|
|
3552
3606
|
return `<pre data-qd-fence="\`\`\`" data-qd-lang="${lang}" data-qd-source="${escapedCode}">${escapedCode}</pre>`;
|
|
3553
3607
|
}
|
|
3554
3608
|
}
|
|
3555
|
-
|
|
3609
|
+
|
|
3556
3610
|
/**
|
|
3557
3611
|
* Parse CSV line handling quoted values
|
|
3558
3612
|
*/
|
|
@@ -3596,13 +3650,13 @@ class QuikdownEditor {
|
|
|
3596
3650
|
try {
|
|
3597
3651
|
const data = JSON.parse(code);
|
|
3598
3652
|
toHighlight = JSON.stringify(data, null, 2);
|
|
3599
|
-
} catch (
|
|
3653
|
+
} catch (_e) {
|
|
3600
3654
|
// Use original if not valid JSON
|
|
3601
3655
|
}
|
|
3602
3656
|
|
|
3603
3657
|
const highlighted = hljs.highlight(toHighlight, { language: 'json' }).value;
|
|
3604
3658
|
return `<pre class="qde-json" data-qd-fence="\`\`\`" data-qd-lang="${lang}"><code class="hljs language-json">${highlighted}</code></pre>`;
|
|
3605
|
-
} catch (
|
|
3659
|
+
} catch (_e) {
|
|
3606
3660
|
// Fall through if highlighting fails
|
|
3607
3661
|
}
|
|
3608
3662
|
}
|
|
@@ -3695,7 +3749,7 @@ class QuikdownEditor {
|
|
|
3695
3749
|
if (loaded) {
|
|
3696
3750
|
renderMap();
|
|
3697
3751
|
} else {
|
|
3698
|
-
const element = document.getElementById(
|
|
3752
|
+
const element = document.getElementById(mapId + '-container');
|
|
3699
3753
|
if (element) {
|
|
3700
3754
|
element.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">Failed to load map library</div>';
|
|
3701
3755
|
}
|
|
@@ -3982,24 +4036,46 @@ class QuikdownEditor {
|
|
|
3982
4036
|
}
|
|
3983
4037
|
|
|
3984
4038
|
/**
|
|
3985
|
-
* Apply theme
|
|
4039
|
+
* Apply the current theme (based on this.options.theme)
|
|
3986
4040
|
*/
|
|
3987
4041
|
applyTheme() {
|
|
3988
4042
|
const theme = this.options.theme;
|
|
3989
|
-
|
|
4043
|
+
|
|
4044
|
+
// Tear down any previous auto-mode listener so we don't stack them
|
|
4045
|
+
if (this._autoThemeListener) {
|
|
4046
|
+
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this._autoThemeListener);
|
|
4047
|
+
this._autoThemeListener = null;
|
|
4048
|
+
}
|
|
4049
|
+
|
|
3990
4050
|
if (theme === 'auto') {
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
this.
|
|
3994
|
-
|
|
3995
|
-
// Listen for changes
|
|
3996
|
-
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
4051
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
4052
|
+
this.container.classList.toggle('qde-dark', mq.matches);
|
|
4053
|
+
this._autoThemeListener = (e) => {
|
|
3997
4054
|
this.container.classList.toggle('qde-dark', e.matches);
|
|
3998
|
-
}
|
|
4055
|
+
};
|
|
4056
|
+
mq.addEventListener('change', this._autoThemeListener);
|
|
3999
4057
|
} else {
|
|
4000
4058
|
this.container.classList.toggle('qde-dark', theme === 'dark');
|
|
4001
4059
|
}
|
|
4002
4060
|
}
|
|
4061
|
+
|
|
4062
|
+
/**
|
|
4063
|
+
* Set theme at runtime. Accepts 'light', 'dark', or 'auto'.
|
|
4064
|
+
* @param {'light'|'dark'|'auto'} theme
|
|
4065
|
+
*/
|
|
4066
|
+
setTheme(theme) {
|
|
4067
|
+
if (!['light', 'dark', 'auto'].includes(theme)) return;
|
|
4068
|
+
this.options.theme = theme;
|
|
4069
|
+
this.applyTheme();
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4072
|
+
/**
|
|
4073
|
+
* Get the current theme option (as configured, not resolved).
|
|
4074
|
+
* @returns {'light'|'dark'|'auto'}
|
|
4075
|
+
*/
|
|
4076
|
+
getTheme() {
|
|
4077
|
+
return this.options.theme;
|
|
4078
|
+
}
|
|
4003
4079
|
|
|
4004
4080
|
/**
|
|
4005
4081
|
* Set lazy linefeeds option
|
|
@@ -4069,6 +4145,105 @@ class QuikdownEditor {
|
|
|
4069
4145
|
}
|
|
4070
4146
|
}
|
|
4071
4147
|
|
|
4148
|
+
// --- Undo / Redo ---
|
|
4149
|
+
|
|
4150
|
+
/**
|
|
4151
|
+
* Push current markdown state onto the undo stack (called before a change).
|
|
4152
|
+
* Only pushes if the new state differs from the current state.
|
|
4153
|
+
* @param {string} newMarkdown - the incoming markdown (used to detect no-op)
|
|
4154
|
+
* @private
|
|
4155
|
+
*/
|
|
4156
|
+
_pushUndoState(newMarkdown) {
|
|
4157
|
+
// Don't push if the content hasn't actually changed
|
|
4158
|
+
if (newMarkdown === this._markdown) return;
|
|
4159
|
+
|
|
4160
|
+
this._undoStack.push(this._markdown);
|
|
4161
|
+
|
|
4162
|
+
// Enforce max stack size
|
|
4163
|
+
const max = this.options.undoStackSize || 100;
|
|
4164
|
+
if (this._undoStack.length > max) {
|
|
4165
|
+
this._undoStack.splice(0, this._undoStack.length - max);
|
|
4166
|
+
}
|
|
4167
|
+
|
|
4168
|
+
// Any new edit clears the redo stack
|
|
4169
|
+
this._redoStack = [];
|
|
4170
|
+
this._updateUndoButtons();
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
/**
|
|
4174
|
+
* Undo the last change. Restores the previous markdown state.
|
|
4175
|
+
*/
|
|
4176
|
+
undo() {
|
|
4177
|
+
if (!this.canUndo()) return;
|
|
4178
|
+
// Save current state to redo stack
|
|
4179
|
+
this._redoStack.push(this._markdown);
|
|
4180
|
+
const previous = this._undoStack.pop();
|
|
4181
|
+
this._isUndoRedo = true;
|
|
4182
|
+
// Update state directly (setMarkdown is async; keep it synchronous here)
|
|
4183
|
+
this._markdown = previous;
|
|
4184
|
+
if (this.sourceTextarea) {
|
|
4185
|
+
this.sourceTextarea.value = previous;
|
|
4186
|
+
}
|
|
4187
|
+
this.updateFromMarkdown(previous);
|
|
4188
|
+
this._updateUndoButtons();
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
/**
|
|
4192
|
+
* Redo the last undone change.
|
|
4193
|
+
*/
|
|
4194
|
+
redo() {
|
|
4195
|
+
if (!this.canRedo()) return;
|
|
4196
|
+
// Save current state to undo stack
|
|
4197
|
+
this._undoStack.push(this._markdown);
|
|
4198
|
+
const next = this._redoStack.pop();
|
|
4199
|
+
this._isUndoRedo = true;
|
|
4200
|
+
this._markdown = next;
|
|
4201
|
+
if (this.sourceTextarea) {
|
|
4202
|
+
this.sourceTextarea.value = next;
|
|
4203
|
+
}
|
|
4204
|
+
this.updateFromMarkdown(next);
|
|
4205
|
+
this._updateUndoButtons();
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
/**
|
|
4209
|
+
* @returns {boolean} true if undo is possible
|
|
4210
|
+
*/
|
|
4211
|
+
canUndo() {
|
|
4212
|
+
return this._undoStack.length > 0;
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
/**
|
|
4216
|
+
* @returns {boolean} true if redo is possible
|
|
4217
|
+
*/
|
|
4218
|
+
canRedo() {
|
|
4219
|
+
return this._redoStack.length > 0;
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
/**
|
|
4223
|
+
* Clear the undo and redo history.
|
|
4224
|
+
*/
|
|
4225
|
+
clearHistory() {
|
|
4226
|
+
this._undoStack = [];
|
|
4227
|
+
this._redoStack = [];
|
|
4228
|
+
this._updateUndoButtons();
|
|
4229
|
+
}
|
|
4230
|
+
|
|
4231
|
+
/**
|
|
4232
|
+
* Update the disabled state of the undo/redo toolbar buttons.
|
|
4233
|
+
* @private
|
|
4234
|
+
*/
|
|
4235
|
+
_updateUndoButtons() {
|
|
4236
|
+
if (!this.toolbar) return;
|
|
4237
|
+
const undoBtn = this.toolbar.querySelector('[data-action="undo"]');
|
|
4238
|
+
const redoBtn = this.toolbar.querySelector('[data-action="redo"]');
|
|
4239
|
+
if (undoBtn) {
|
|
4240
|
+
undoBtn.classList.toggle('disabled', !this.canUndo());
|
|
4241
|
+
}
|
|
4242
|
+
if (redoBtn) {
|
|
4243
|
+
redoBtn.classList.toggle('disabled', !this.canRedo());
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4072
4247
|
/**
|
|
4073
4248
|
* Handle toolbar actions
|
|
4074
4249
|
*/
|
|
@@ -4086,6 +4261,15 @@ class QuikdownEditor {
|
|
|
4086
4261
|
case 'remove-hr':
|
|
4087
4262
|
this.removeHR();
|
|
4088
4263
|
break;
|
|
4264
|
+
case 'lazy-linefeeds':
|
|
4265
|
+
this.convertLazyLinefeeds();
|
|
4266
|
+
break;
|
|
4267
|
+
case 'undo':
|
|
4268
|
+
this.undo();
|
|
4269
|
+
break;
|
|
4270
|
+
case 'redo':
|
|
4271
|
+
this.redo();
|
|
4272
|
+
break;
|
|
4089
4273
|
}
|
|
4090
4274
|
}
|
|
4091
4275
|
|
|
@@ -4173,24 +4357,13 @@ class QuikdownEditor {
|
|
|
4173
4357
|
}
|
|
4174
4358
|
|
|
4175
4359
|
/**
|
|
4176
|
-
* Remove all horizontal rules (---) from markdown
|
|
4360
|
+
* Remove all horizontal rules (---) from markdown source.
|
|
4361
|
+
* Preserves content inside fences (``` or ~~~) and table separator rows.
|
|
4177
4362
|
*/
|
|
4178
4363
|
async removeHR() {
|
|
4179
|
-
|
|
4180
|
-
// Matches: ---, ___, ***, ----, etc. with optional spaces
|
|
4181
|
-
const cleaned = this._markdown
|
|
4182
|
-
.split('\n')
|
|
4183
|
-
.filter(line => {
|
|
4184
|
-
// Keep lines that aren't just HR patterns
|
|
4185
|
-
const trimmed = line.trim();
|
|
4186
|
-
// Match HR patterns: 3+ of -, _, or * with optional spaces between
|
|
4187
|
-
return !(/^[-_*](\s*[-_*]){2,}\s*$/.test(trimmed));
|
|
4188
|
-
})
|
|
4189
|
-
.join('\n');
|
|
4190
|
-
|
|
4191
|
-
// Update the markdown
|
|
4364
|
+
const cleaned = QuikdownEditor.removeHRFromMarkdown(this._markdown);
|
|
4192
4365
|
await this.setMarkdown(cleaned);
|
|
4193
|
-
|
|
4366
|
+
|
|
4194
4367
|
// Visual feedback if toolbar button exists
|
|
4195
4368
|
const btn = this.toolbar?.querySelector('[data-action="remove-hr"]');
|
|
4196
4369
|
if (btn) {
|
|
@@ -4201,6 +4374,202 @@ class QuikdownEditor {
|
|
|
4201
4374
|
}, 1500);
|
|
4202
4375
|
}
|
|
4203
4376
|
}
|
|
4377
|
+
|
|
4378
|
+
/**
|
|
4379
|
+
* Static: remove horizontal rules from markdown string.
|
|
4380
|
+
* Safe for fences, tables, and all markdown constructs.
|
|
4381
|
+
* Can be used headless without an editor instance.
|
|
4382
|
+
* @param {string} markdown - source markdown
|
|
4383
|
+
* @returns {string} markdown with standalone HRs removed
|
|
4384
|
+
*/
|
|
4385
|
+
static removeHRFromMarkdown(markdown) {
|
|
4386
|
+
const lines = (markdown || '').split('\n');
|
|
4387
|
+
const result = [];
|
|
4388
|
+
let inFence = false;
|
|
4389
|
+
let fenceChar = null; // '`' or '~'
|
|
4390
|
+
let fenceLen = 0; // length of opening fence marker
|
|
4391
|
+
|
|
4392
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4393
|
+
const line = lines[i];
|
|
4394
|
+
const trimmed = line.trim();
|
|
4395
|
+
|
|
4396
|
+
// Track fence open/close (``` or ~~~, 3+ chars)
|
|
4397
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
4398
|
+
if (fenceMatch) {
|
|
4399
|
+
const matchChar = fenceMatch[1][0];
|
|
4400
|
+
const matchLen = fenceMatch[1].length;
|
|
4401
|
+
if (!inFence) {
|
|
4402
|
+
inFence = true;
|
|
4403
|
+
fenceChar = matchChar;
|
|
4404
|
+
fenceLen = matchLen;
|
|
4405
|
+
result.push(line);
|
|
4406
|
+
continue;
|
|
4407
|
+
} else if (matchChar === fenceChar && matchLen >= fenceLen && /^(`{3,}|~{3,})\s*$/.test(trimmed)) {
|
|
4408
|
+
// Closing fence: same char, at least as many chars, no trailing content
|
|
4409
|
+
inFence = false;
|
|
4410
|
+
fenceChar = null;
|
|
4411
|
+
fenceLen = 0;
|
|
4412
|
+
result.push(line);
|
|
4413
|
+
continue;
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
// Inside a fence — keep everything
|
|
4418
|
+
if (inFence) {
|
|
4419
|
+
result.push(line);
|
|
4420
|
+
continue;
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
// Detect table row/separator with pipes — always keep
|
|
4424
|
+
if (/^\|.*\|$/.test(trimmed) || (/^[-| :]+$/.test(trimmed) && trimmed.includes('|'))) {
|
|
4425
|
+
result.push(line);
|
|
4426
|
+
continue;
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
// Check if this line is a standalone HR
|
|
4430
|
+
const isHR = /^[-_*](\s*[-_*]){2,}\s*$/.test(trimmed);
|
|
4431
|
+
if (isHR) {
|
|
4432
|
+
// Table separator heuristic: immediately adjacent lines (no blank
|
|
4433
|
+
// lines between) that look like table rows protect this HR-like line
|
|
4434
|
+
const prevLine = i > 0 ? lines[i - 1].trim() : '';
|
|
4435
|
+
const nextLine = i < lines.length - 1 ? lines[i + 1].trim() : '';
|
|
4436
|
+
if (_looksLikeTableRow(prevLine) || _looksLikeTableRow(nextLine)) {
|
|
4437
|
+
result.push(line);
|
|
4438
|
+
continue;
|
|
4439
|
+
}
|
|
4440
|
+
// It's a real HR — skip it
|
|
4441
|
+
continue;
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
result.push(line);
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
return result.join('\n');
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
/**
|
|
4451
|
+
* Convert lazy linefeeds in markdown source.
|
|
4452
|
+
* Replaces single newlines with double newlines (adds real line breaks)
|
|
4453
|
+
* except inside fences, tables, and other block-level constructs.
|
|
4454
|
+
* Idempotent: calling multiple times produces the same result.
|
|
4455
|
+
* Can be used as a toolbar action or headless via the static method.
|
|
4456
|
+
*/
|
|
4457
|
+
async convertLazyLinefeeds() {
|
|
4458
|
+
const converted = QuikdownEditor.convertLazyLinefeeds(this._markdown);
|
|
4459
|
+
await this.setMarkdown(converted);
|
|
4460
|
+
|
|
4461
|
+
// Visual feedback if toolbar button exists
|
|
4462
|
+
const btn = this.toolbar?.querySelector('[data-action="lazy-linefeeds"]');
|
|
4463
|
+
if (btn) {
|
|
4464
|
+
const originalText = btn.textContent;
|
|
4465
|
+
btn.textContent = 'Converted!';
|
|
4466
|
+
setTimeout(() => {
|
|
4467
|
+
btn.textContent = originalText;
|
|
4468
|
+
}, 1500);
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
/**
|
|
4473
|
+
* Static: convert lazy linefeeds in markdown source.
|
|
4474
|
+
* Turns single \n between non-blank lines into \n\n so each line becomes
|
|
4475
|
+
* its own paragraph / hard break. Idempotent — already-doubled newlines
|
|
4476
|
+
* are not doubled again. Fences, tables, lists, blockquotes, headings,
|
|
4477
|
+
* and HTML blocks are left untouched.
|
|
4478
|
+
* @param {string} markdown - source markdown
|
|
4479
|
+
* @returns {string} markdown with lazy linefeeds resolved
|
|
4480
|
+
*/
|
|
4481
|
+
static convertLazyLinefeeds(markdown) {
|
|
4482
|
+
const lines = (markdown || '').split('\n');
|
|
4483
|
+
const result = [];
|
|
4484
|
+
let inFence = false;
|
|
4485
|
+
let fenceChar = null;
|
|
4486
|
+
let fenceLen = 0;
|
|
4487
|
+
let inHTMLBlock = false;
|
|
4488
|
+
|
|
4489
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4490
|
+
const line = lines[i];
|
|
4491
|
+
const trimmed = line.trim();
|
|
4492
|
+
|
|
4493
|
+
// Track fence open/close
|
|
4494
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
4495
|
+
if (fenceMatch) {
|
|
4496
|
+
const matchChar = fenceMatch[1][0];
|
|
4497
|
+
const matchLen = fenceMatch[1].length;
|
|
4498
|
+
if (!inFence) {
|
|
4499
|
+
inFence = true;
|
|
4500
|
+
fenceChar = matchChar;
|
|
4501
|
+
fenceLen = matchLen;
|
|
4502
|
+
result.push(line);
|
|
4503
|
+
continue;
|
|
4504
|
+
} else if (matchChar === fenceChar && matchLen >= fenceLen && /^(`{3,}|~{3,})\s*$/.test(trimmed)) {
|
|
4505
|
+
inFence = false;
|
|
4506
|
+
fenceChar = null;
|
|
4507
|
+
fenceLen = 0;
|
|
4508
|
+
result.push(line);
|
|
4509
|
+
continue;
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
// Inside fence — pass through
|
|
4514
|
+
if (inFence) {
|
|
4515
|
+
result.push(line);
|
|
4516
|
+
continue;
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4519
|
+
// Track HTML blocks (lines starting with < and ending with >)
|
|
4520
|
+
if (/^<[a-zA-Z]/.test(trimmed)) inHTMLBlock = true;
|
|
4521
|
+
if (inHTMLBlock) {
|
|
4522
|
+
result.push(line);
|
|
4523
|
+
if (/>$/.test(trimmed) || trimmed === '') inHTMLBlock = false;
|
|
4524
|
+
continue;
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4527
|
+
// Always pass through blank lines, but never add extras
|
|
4528
|
+
if (trimmed === '') {
|
|
4529
|
+
// Avoid doubling: don't add blank line if the last result line is already blank
|
|
4530
|
+
if (result.length === 0 || result[result.length - 1].trim() !== '') {
|
|
4531
|
+
result.push(line);
|
|
4532
|
+
}
|
|
4533
|
+
continue;
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
// Skip conversion for block-level constructs
|
|
4537
|
+
const isBlockElement = (
|
|
4538
|
+
/^#{1,6}\s/.test(trimmed) || // headings
|
|
4539
|
+
/^[-_*](\s*[-_*]){2,}\s*$/.test(trimmed) || // horizontal rules
|
|
4540
|
+
/^(\d+\.|-|\*|\+)\s/.test(trimmed) || // list items
|
|
4541
|
+
/^>/.test(trimmed) || // blockquotes
|
|
4542
|
+
/^\|/.test(trimmed) // table rows
|
|
4543
|
+
);
|
|
4544
|
+
|
|
4545
|
+
if (isBlockElement) {
|
|
4546
|
+
result.push(line);
|
|
4547
|
+
continue;
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4550
|
+
// For plain paragraph text: if previous result line is non-blank
|
|
4551
|
+
// plain text, insert a blank line between them (making the single
|
|
4552
|
+
// newline into a paragraph break). This is the lazy→strict conversion.
|
|
4553
|
+
if (result.length > 0) {
|
|
4554
|
+
const prevLine = result[result.length - 1];
|
|
4555
|
+
const prevTrimmed = prevLine.trim();
|
|
4556
|
+
// Only insert blank line if prev is non-blank, non-block text
|
|
4557
|
+
if (prevTrimmed !== '' &&
|
|
4558
|
+
!/^#{1,6}\s/.test(prevTrimmed) &&
|
|
4559
|
+
!/^[-_*](\s*[-_*]){2,}\s*$/.test(prevTrimmed) &&
|
|
4560
|
+
!/^(\d+\.|-|\*|\+)\s/.test(prevTrimmed) &&
|
|
4561
|
+
!/^>/.test(prevTrimmed) &&
|
|
4562
|
+
!/^\|/.test(prevTrimmed) &&
|
|
4563
|
+
!/^(`{3,}|~{3,})/.test(prevTrimmed)) {
|
|
4564
|
+
result.push('');
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
result.push(line);
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
return result.join('\n');
|
|
4572
|
+
}
|
|
4204
4573
|
|
|
4205
4574
|
/**
|
|
4206
4575
|
* Copy rendered content as rich text
|
|
@@ -4244,6 +4613,13 @@ class QuikdownEditor {
|
|
|
4244
4613
|
}
|
|
4245
4614
|
}
|
|
4246
4615
|
|
|
4616
|
+
// --- Internal helpers for removeHR fence/table awareness ---
|
|
4617
|
+
|
|
4618
|
+
/** Heuristic: does this line look like a markdown table row? */
|
|
4619
|
+
function _looksLikeTableRow(line) {
|
|
4620
|
+
return line.includes('|');
|
|
4621
|
+
}
|
|
4622
|
+
|
|
4247
4623
|
// Export for CommonJS (needed for bundled ESM to work with Jest)
|
|
4248
4624
|
if (typeof module !== 'undefined' && module.exports) {
|
|
4249
4625
|
module.exports = QuikdownEditor;
|