quikdown 1.2.2 → 1.2.7
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 +22 -27
- package/dist/quikdown.cjs +25 -9
- package/dist/quikdown.dark.css +1 -1
- package/dist/quikdown.esm.js +25 -9
- package/dist/quikdown.esm.min.js +2 -2
- package/dist/quikdown.esm.min.js.gz +0 -0
- package/dist/quikdown.esm.min.js.map +1 -1
- package/dist/quikdown.light.css +1 -1
- package/dist/quikdown.umd.js +25 -9
- package/dist/quikdown.umd.min.js +2 -2
- package/dist/quikdown.umd.min.js.gz +0 -0
- package/dist/quikdown.umd.min.js.map +1 -1
- package/dist/quikdown_ast.cjs +2 -2
- package/dist/quikdown_ast.esm.js +2 -2
- package/dist/quikdown_ast.esm.min.js +2 -2
- package/dist/quikdown_ast.esm.min.js.gz +0 -0
- package/dist/quikdown_ast.umd.js +2 -2
- package/dist/quikdown_ast.umd.min.js +2 -2
- package/dist/quikdown_ast.umd.min.js.gz +0 -0
- package/dist/quikdown_ast_html.cjs +3 -3
- package/dist/quikdown_ast_html.esm.js +3 -3
- package/dist/quikdown_ast_html.esm.min.js +2 -2
- package/dist/quikdown_ast_html.esm.min.js.gz +0 -0
- package/dist/quikdown_ast_html.umd.js +3 -3
- package/dist/quikdown_ast_html.umd.min.js +2 -2
- package/dist/quikdown_ast_html.umd.min.js.gz +0 -0
- package/dist/quikdown_bd.cjs +34 -12
- package/dist/quikdown_bd.esm.js +34 -12
- package/dist/quikdown_bd.esm.min.js +2 -2
- package/dist/quikdown_bd.esm.min.js.gz +0 -0
- package/dist/quikdown_bd.esm.min.js.map +1 -1
- package/dist/quikdown_bd.umd.js +34 -12
- package/dist/quikdown_bd.umd.min.js +2 -2
- package/dist/quikdown_bd.umd.min.js.gz +0 -0
- package/dist/quikdown_bd.umd.min.js.map +1 -1
- package/dist/quikdown_edit.cjs +471 -124
- package/dist/quikdown_edit.d.ts +15 -1
- package/dist/quikdown_edit.esm.js +471 -124
- package/dist/quikdown_edit.esm.min.js +3 -3
- package/dist/quikdown_edit.esm.min.js.gz +0 -0
- package/dist/quikdown_edit.esm.min.js.map +1 -1
- package/dist/quikdown_edit.umd.js +471 -124
- package/dist/quikdown_edit.umd.min.js +3 -3
- package/dist/quikdown_edit.umd.min.js.gz +0 -0
- package/dist/quikdown_edit.umd.min.js.map +1 -1
- package/dist/quikdown_json.cjs +3 -3
- package/dist/quikdown_json.esm.js +3 -3
- package/dist/quikdown_json.esm.min.js +2 -2
- package/dist/quikdown_json.esm.min.js.gz +0 -0
- package/dist/quikdown_json.umd.js +3 -3
- package/dist/quikdown_json.umd.min.js +2 -2
- package/dist/quikdown_json.umd.min.js.gz +0 -0
- package/dist/quikdown_yaml.cjs +3 -3
- package/dist/quikdown_yaml.esm.js +3 -3
- package/dist/quikdown_yaml.esm.min.js +2 -2
- package/dist/quikdown_yaml.esm.min.js.gz +0 -0
- package/dist/quikdown_yaml.umd.js +3 -3
- package/dist/quikdown_yaml.umd.min.js +2 -2
- package/dist/quikdown_yaml.umd.min.js.gz +0 -0
- package/package.json +17 -14
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quikdown Editor - Drop-in Markdown Parser
|
|
3
|
-
* @version 1.2.
|
|
3
|
+
* @version 1.2.7
|
|
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.2.
|
|
27
|
+
const quikdownVersion = '1.2.7';
|
|
28
28
|
|
|
29
29
|
// Constants for reuse
|
|
30
30
|
const CLASS_PREFIX = 'quikdown-';
|
|
@@ -72,6 +72,11 @@
|
|
|
72
72
|
// Remove default text-align if we're adding a different alignment
|
|
73
73
|
if (additionalStyle && additionalStyle.includes('text-align') && style && style.includes('text-align')) {
|
|
74
74
|
style = style.replace(/text-align:[^;]+;?/, '').trim();
|
|
75
|
+
// Ensure trailing semicolon before concatenating additionalStyle.
|
|
76
|
+
// Both short-circuit paths of this guard (empty `style` or
|
|
77
|
+
// already-has-`;`) are defensive and unreachable with the
|
|
78
|
+
// current QUIKDOWN_STYLES values — istanbul ignore next.
|
|
79
|
+
/* istanbul ignore next */
|
|
75
80
|
if (style && !style.endsWith(';')) style += ';';
|
|
76
81
|
}
|
|
77
82
|
|
|
@@ -94,7 +99,7 @@
|
|
|
94
99
|
return '';
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
const { fence_plugin, inline_styles = false, bidirectional = false, lazy_linefeeds = false } = options;
|
|
102
|
+
const { fence_plugin, inline_styles = false, bidirectional = false, lazy_linefeeds = false, allow_unsafe_html = false } = options;
|
|
98
103
|
const styles = QUIKDOWN_STYLES; // Use module-level styles
|
|
99
104
|
const getAttr = createGetAttr(inline_styles, styles); // Create getAttr once
|
|
100
105
|
|
|
@@ -103,9 +108,12 @@
|
|
|
103
108
|
return text.replace(/[&<>"']/g, m => ESC_MAP[m]);
|
|
104
109
|
}
|
|
105
110
|
|
|
106
|
-
// Helper to add data-qd attributes for bidirectional support
|
|
111
|
+
// Helper to add data-qd attributes for bidirectional support.
|
|
112
|
+
// The non-bidirectional branch is a trivial no-op arrow; it's exercised in
|
|
113
|
+
// the core bundle but never in quikdown_bd (which always sets bidirectional=true).
|
|
114
|
+
/* istanbul ignore next - trivial no-op fallback */
|
|
107
115
|
const dataQd = bidirectional ? (marker) => ` data-qd="${escapeHtml(marker)}"` : () => '';
|
|
108
|
-
|
|
116
|
+
|
|
109
117
|
// Sanitize URLs to prevent XSS attacks
|
|
110
118
|
function sanitizeUrl(url, allowUnsafe = false) {
|
|
111
119
|
/* istanbul ignore next - defensive programming, regex ensures url is never empty */
|
|
@@ -177,8 +185,11 @@
|
|
|
177
185
|
return placeholder;
|
|
178
186
|
});
|
|
179
187
|
|
|
180
|
-
//
|
|
181
|
-
|
|
188
|
+
// Escape HTML in the rest of the content (skip if allow_unsafe_html is on —
|
|
189
|
+
// useful for trusted pipelines where the markdown contains intentional HTML)
|
|
190
|
+
if (!allow_unsafe_html) {
|
|
191
|
+
html = escapeHtml(html);
|
|
192
|
+
}
|
|
182
193
|
|
|
183
194
|
// Phase 2: Process block elements
|
|
184
195
|
|
|
@@ -511,8 +522,13 @@
|
|
|
511
522
|
const result = [];
|
|
512
523
|
const listStack = []; // Track nested lists
|
|
513
524
|
|
|
514
|
-
// Helper to escape HTML for data-qd attributes
|
|
515
|
-
|
|
525
|
+
// Helper to escape HTML for data-qd attributes. List markers (`-`, `*`,
|
|
526
|
+
// `+`, `1.`, etc.) never contain HTML-special chars, so the replace
|
|
527
|
+
// callback is defensive-only and never actually fires in practice.
|
|
528
|
+
const escapeHtml = (text) => text.replace(/[&<>"']/g,
|
|
529
|
+
/* istanbul ignore next - defensive: list markers never contain HTML specials */
|
|
530
|
+
m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]);
|
|
531
|
+
/* istanbul ignore next - trivial no-op fallback; not exercised via bd bundle */
|
|
516
532
|
const dataQd = bidirectional ? (marker) => ` data-qd="${escapeHtml(marker)}"` : () => '';
|
|
517
533
|
|
|
518
534
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -684,8 +700,11 @@
|
|
|
684
700
|
return quikdown(markdown, { ...options, bidirectional: true });
|
|
685
701
|
}
|
|
686
702
|
|
|
687
|
-
// Copy all properties and methods from quikdown (including version)
|
|
703
|
+
// Copy all properties and methods from quikdown (including version).
|
|
704
|
+
// Skip `configure` — quikdown_bd provides its own override below, so the
|
|
705
|
+
// inner quikdown.configure is dead code in this bundle.
|
|
688
706
|
Object.keys(quikdown).forEach(key => {
|
|
707
|
+
if (key === 'configure') return;
|
|
689
708
|
quikdown_bd[key] = quikdown[key];
|
|
690
709
|
});
|
|
691
710
|
|
|
@@ -1049,10 +1068,13 @@
|
|
|
1049
1068
|
return markdown;
|
|
1050
1069
|
};
|
|
1051
1070
|
|
|
1052
|
-
// Override the configure method to return a bidirectional version
|
|
1071
|
+
// Override the configure method to return a bidirectional version.
|
|
1072
|
+
// We delegate to the inner quikdown.configure so the shared closure
|
|
1073
|
+
// machinery is exercised in both bundles (no dead code).
|
|
1053
1074
|
quikdown_bd.configure = function(options) {
|
|
1075
|
+
const innerParser = quikdown.configure({ ...options, bidirectional: true });
|
|
1054
1076
|
return function(markdown) {
|
|
1055
|
-
return
|
|
1077
|
+
return innerParser(markdown);
|
|
1056
1078
|
};
|
|
1057
1079
|
};
|
|
1058
1080
|
|
|
@@ -2558,12 +2580,75 @@
|
|
|
2558
2580
|
highlightjs: false,
|
|
2559
2581
|
mermaid: false
|
|
2560
2582
|
},
|
|
2583
|
+
/**
|
|
2584
|
+
* Preload fence-rendering libraries at construction time so the FIRST
|
|
2585
|
+
* encounter with a fence type renders instantly (no lazy load delay).
|
|
2586
|
+
*
|
|
2587
|
+
* Accepts:
|
|
2588
|
+
* - 'all' — preload every known library
|
|
2589
|
+
* - ['highlightjs','mermaid','math',
|
|
2590
|
+
* 'geojson','stl'] — preload specific libraries
|
|
2591
|
+
* - [{ name: 'mylib', script: 'https://...', css: '...' }]
|
|
2592
|
+
* — preload an arbitrary library
|
|
2593
|
+
*
|
|
2594
|
+
* Without this, fence libraries are loaded on demand the first time their
|
|
2595
|
+
* fence type is encountered. That keeps the editor lightweight, but the
|
|
2596
|
+
* first SVG/Mermaid/Math/GeoJSON/STL fence will show "loading..." for a
|
|
2597
|
+
* moment. Set `preloadFences` if you want zero-delay rendering — at the
|
|
2598
|
+
* cost of a few hundred KB of upfront network.
|
|
2599
|
+
*
|
|
2600
|
+
* Developer's choice. The editor itself is still ~70 KB minified;
|
|
2601
|
+
* `preloadFences` only affects the OPTIONAL fence renderers.
|
|
2602
|
+
*/
|
|
2603
|
+
preloadFences: null,
|
|
2561
2604
|
customFences: {}, // { 'language': (code, lang) => html }
|
|
2562
2605
|
enableComplexFences: true, // Enable CSV tables, math rendering, SVG, etc.
|
|
2563
2606
|
showUndoRedo: false, // Show undo/redo toolbar buttons
|
|
2564
2607
|
undoStackSize: 100 // Maximum number of undo states to keep
|
|
2565
2608
|
};
|
|
2566
2609
|
|
|
2610
|
+
// Library catalog used by preloadFences. Each entry knows how to:
|
|
2611
|
+
// - check if the library is already on the page (so we don't double-load)
|
|
2612
|
+
// - load it via script (and optional CSS)
|
|
2613
|
+
const FENCE_LIBRARIES = {
|
|
2614
|
+
highlightjs: {
|
|
2615
|
+
check: () => typeof window.hljs !== 'undefined',
|
|
2616
|
+
script: 'https://unpkg.com/@highlightjs/cdn-assets/highlight.min.js',
|
|
2617
|
+
css: 'https://unpkg.com/@highlightjs/cdn-assets/styles/github.min.css',
|
|
2618
|
+
cssDark: 'https://unpkg.com/@highlightjs/cdn-assets/styles/github-dark.min.css'
|
|
2619
|
+
},
|
|
2620
|
+
mermaid: {
|
|
2621
|
+
check: () => typeof window.mermaid !== 'undefined',
|
|
2622
|
+
script: 'https://unpkg.com/mermaid/dist/mermaid.min.js',
|
|
2623
|
+
afterLoad: () => {
|
|
2624
|
+
if (window.mermaid) window.mermaid.initialize({ startOnLoad: false });
|
|
2625
|
+
}
|
|
2626
|
+
},
|
|
2627
|
+
math: {
|
|
2628
|
+
check: () => typeof window.MathJax !== 'undefined',
|
|
2629
|
+
script: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js',
|
|
2630
|
+
beforeLoad: () => {
|
|
2631
|
+
// Configure MathJax before loading (must be set on window before script runs)
|
|
2632
|
+
if (!window.MathJax) {
|
|
2633
|
+
window.MathJax = {
|
|
2634
|
+
tex: { inlineMath: [['$', '$'], ['\\(', '\\)']], displayMath: [['$$', '$$'], ['\\[', '\\]']] },
|
|
2635
|
+
svg: { fontCache: 'global' },
|
|
2636
|
+
startup: { typeset: false }
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
},
|
|
2641
|
+
geojson: {
|
|
2642
|
+
check: () => typeof window.L !== 'undefined',
|
|
2643
|
+
script: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js',
|
|
2644
|
+
css: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'
|
|
2645
|
+
},
|
|
2646
|
+
stl: {
|
|
2647
|
+
check: () => typeof window.THREE !== 'undefined',
|
|
2648
|
+
script: 'https://unpkg.com/three@0.147.0/build/three.min.js'
|
|
2649
|
+
}
|
|
2650
|
+
};
|
|
2651
|
+
|
|
2567
2652
|
/**
|
|
2568
2653
|
* Quikdown Editor - A complete markdown editing solution
|
|
2569
2654
|
*/
|
|
@@ -2647,6 +2732,7 @@
|
|
|
2647
2732
|
|
|
2648
2733
|
this.sourceTextarea = document.createElement('textarea');
|
|
2649
2734
|
this.sourceTextarea.className = 'qde-textarea';
|
|
2735
|
+
this.sourceTextarea.spellcheck = false;
|
|
2650
2736
|
this.sourceTextarea.placeholder = this.options.placeholder;
|
|
2651
2737
|
this.sourcePanel.appendChild(this.sourceTextarea);
|
|
2652
2738
|
|
|
@@ -2654,6 +2740,7 @@
|
|
|
2654
2740
|
this.previewPanel = document.createElement('div');
|
|
2655
2741
|
this.previewPanel.className = 'qde-preview';
|
|
2656
2742
|
this.previewPanel.contentEditable = true;
|
|
2743
|
+
this.previewPanel.spellcheck = false;
|
|
2657
2744
|
|
|
2658
2745
|
// Add panels to editor
|
|
2659
2746
|
this.editorArea.appendChild(this.sourcePanel);
|
|
@@ -2761,6 +2848,7 @@
|
|
|
2761
2848
|
border-radius: 4px;
|
|
2762
2849
|
overflow: hidden;
|
|
2763
2850
|
background: white;
|
|
2851
|
+
color: #1f2937;
|
|
2764
2852
|
}
|
|
2765
2853
|
|
|
2766
2854
|
.qde-toolbar {
|
|
@@ -2809,24 +2897,45 @@
|
|
|
2809
2897
|
}
|
|
2810
2898
|
|
|
2811
2899
|
.qde-source, .qde-preview {
|
|
2812
|
-
flex: 1;
|
|
2900
|
+
flex: 1 1 0;
|
|
2901
|
+
min-width: 0; /* allow flex shrinking below content size */
|
|
2902
|
+
min-height: 0;
|
|
2813
2903
|
overflow: auto;
|
|
2814
2904
|
padding: 16px;
|
|
2905
|
+
box-sizing: border-box;
|
|
2815
2906
|
}
|
|
2816
|
-
|
|
2907
|
+
|
|
2817
2908
|
.qde-source {
|
|
2818
2909
|
border-right: 1px solid #ddd;
|
|
2910
|
+
/* Source pane is just a container for the textarea — make it
|
|
2911
|
+
a positioning context so the textarea can fill it absolutely */
|
|
2912
|
+
position: relative;
|
|
2913
|
+
padding: 0; /* textarea brings its own padding */
|
|
2819
2914
|
}
|
|
2820
|
-
|
|
2915
|
+
|
|
2821
2916
|
.qde-textarea {
|
|
2917
|
+
display: block;
|
|
2918
|
+
position: absolute;
|
|
2919
|
+
inset: 0;
|
|
2822
2920
|
width: 100%;
|
|
2823
2921
|
height: 100%;
|
|
2824
2922
|
border: none;
|
|
2825
2923
|
outline: none;
|
|
2826
2924
|
resize: none;
|
|
2925
|
+
padding: 16px;
|
|
2926
|
+
box-sizing: border-box;
|
|
2827
2927
|
font-family: 'Monaco', 'Courier New', monospace;
|
|
2828
2928
|
font-size: 14px;
|
|
2829
2929
|
line-height: 1.5;
|
|
2930
|
+
background: transparent;
|
|
2931
|
+
color: inherit;
|
|
2932
|
+
/* Wrap long lines so the textarea only scrolls VERTICALLY.
|
|
2933
|
+
pre-wrap preserves intentional line breaks/whitespace
|
|
2934
|
+
while soft-wrapping at the right edge. */
|
|
2935
|
+
white-space: pre-wrap;
|
|
2936
|
+
word-wrap: break-word;
|
|
2937
|
+
overflow-x: hidden;
|
|
2938
|
+
overflow-y: auto;
|
|
2830
2939
|
}
|
|
2831
2940
|
|
|
2832
2941
|
.qde-preview {
|
|
@@ -2835,14 +2944,69 @@
|
|
|
2835
2944
|
line-height: 1.6;
|
|
2836
2945
|
outline: none;
|
|
2837
2946
|
cursor: text; /* Standard text cursor */
|
|
2947
|
+
overflow-x: hidden; /* never scroll horizontally; clip wide content */
|
|
2838
2948
|
}
|
|
2839
|
-
|
|
2949
|
+
|
|
2950
|
+
/* Code blocks and inline code — self-contained so the editor
|
|
2951
|
+
does not depend on any external stylesheet for these. */
|
|
2952
|
+
.qde-preview pre {
|
|
2953
|
+
background: #f4f4f4;
|
|
2954
|
+
color: #1f2937;
|
|
2955
|
+
padding: 10px;
|
|
2956
|
+
border-radius: 4px;
|
|
2957
|
+
overflow-x: auto;
|
|
2958
|
+
margin: 0.6em 0;
|
|
2959
|
+
font-size: 0.9em;
|
|
2960
|
+
line-height: 1.5;
|
|
2961
|
+
font-family: ui-monospace, "SF Mono", Monaco, "Cascadia Code",
|
|
2962
|
+
"Roboto Mono", Consolas, "Courier New", monospace;
|
|
2963
|
+
}
|
|
2964
|
+
.qde-preview code {
|
|
2965
|
+
padding: 2px 4px;
|
|
2966
|
+
font-size: 0.9em;
|
|
2967
|
+
border-radius: 3px;
|
|
2968
|
+
background: #f0f0f0;
|
|
2969
|
+
color: #1f2937;
|
|
2970
|
+
font-family: ui-monospace, "SF Mono", Monaco, "Cascadia Code",
|
|
2971
|
+
"Roboto Mono", Consolas, "Courier New", monospace;
|
|
2972
|
+
}
|
|
2973
|
+
.qde-preview pre code {
|
|
2974
|
+
padding: 0;
|
|
2975
|
+
font-size: inherit;
|
|
2976
|
+
border-radius: 0;
|
|
2977
|
+
background: transparent;
|
|
2978
|
+
color: inherit;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
/* Wide fence content (Leaflet maps, large SVGs, STL canvases,
|
|
2982
|
+
iframes, raw <img>) must never overflow the preview pane */
|
|
2983
|
+
.qde-preview .geojson-container,
|
|
2984
|
+
.qde-preview .qde-stl-container,
|
|
2985
|
+
.qde-preview .qde-svg-container,
|
|
2986
|
+
.qde-preview .leaflet-container,
|
|
2987
|
+
.qde-preview iframe,
|
|
2988
|
+
.qde-preview img,
|
|
2989
|
+
.qde-preview > svg {
|
|
2990
|
+
max-width: 100%;
|
|
2991
|
+
}
|
|
2992
|
+
.qde-preview .leaflet-container { box-sizing: border-box; }
|
|
2993
|
+
|
|
2994
|
+
/* Standard markdown tables (the .quikdown-table class) need to
|
|
2995
|
+
scroll horizontally inside their own wrapper rather than
|
|
2996
|
+
making the whole preview pane scroll */
|
|
2997
|
+
.qde-preview table.quikdown-table,
|
|
2998
|
+
.qde-preview table.qde-csv-table {
|
|
2999
|
+
display: block;
|
|
3000
|
+
max-width: 100%;
|
|
3001
|
+
overflow-x: auto;
|
|
3002
|
+
}
|
|
3003
|
+
|
|
2840
3004
|
/* Fence-specific styles */
|
|
2841
3005
|
.qde-svg-container {
|
|
2842
3006
|
max-width: 100%;
|
|
2843
3007
|
overflow: auto;
|
|
2844
3008
|
}
|
|
2845
|
-
|
|
3009
|
+
|
|
2846
3010
|
.qde-svg-container svg {
|
|
2847
3011
|
max-width: 100%;
|
|
2848
3012
|
height: auto;
|
|
@@ -2914,6 +3078,45 @@
|
|
|
2914
3078
|
position: relative;
|
|
2915
3079
|
}
|
|
2916
3080
|
|
|
3081
|
+
/* Reset headings inside the preview to plain browser defaults so
|
|
3082
|
+
parent-page styles (site navs, marketing pages, design systems)
|
|
3083
|
+
cannot bleed in. Business-casual: black text, decreasing sizes,
|
|
3084
|
+
no decorative borders. See docs/quikdown-editor.md for how
|
|
3085
|
+
embedders can override these with their own stylesheet. */
|
|
3086
|
+
.qde-preview h1 { font-size: 2em; }
|
|
3087
|
+
.qde-preview h2 { font-size: 1.5em; }
|
|
3088
|
+
.qde-preview h3 { font-size: 1.25em; }
|
|
3089
|
+
.qde-preview h4 { font-size: 1em; }
|
|
3090
|
+
.qde-preview h5 { font-size: 0.875em; }
|
|
3091
|
+
.qde-preview h6 { font-size: 0.85em; }
|
|
3092
|
+
.qde-preview h1,
|
|
3093
|
+
.qde-preview h2,
|
|
3094
|
+
.qde-preview h3,
|
|
3095
|
+
.qde-preview h4,
|
|
3096
|
+
.qde-preview h5,
|
|
3097
|
+
.qde-preview h6 {
|
|
3098
|
+
font-weight: bold;
|
|
3099
|
+
color: inherit;
|
|
3100
|
+
border: none;
|
|
3101
|
+
margin: 0.6em 0 0.3em 0;
|
|
3102
|
+
line-height: 1.25;
|
|
3103
|
+
}
|
|
3104
|
+
.qde-preview p {
|
|
3105
|
+
margin: 0.35em 0;
|
|
3106
|
+
}
|
|
3107
|
+
.qde-preview ul,
|
|
3108
|
+
.qde-preview ol {
|
|
3109
|
+
padding-left: 1.8em;
|
|
3110
|
+
margin: 0.4em 0;
|
|
3111
|
+
}
|
|
3112
|
+
.qde-preview li {
|
|
3113
|
+
margin: 0.15em 0;
|
|
3114
|
+
}
|
|
3115
|
+
.qde-preview blockquote {
|
|
3116
|
+
margin: 0.5em 0;
|
|
3117
|
+
padding-left: 1em;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
2917
3120
|
/* Ensure proper cursor for editable text elements */
|
|
2918
3121
|
.qde-preview p,
|
|
2919
3122
|
.qde-preview h1,
|
|
@@ -2976,6 +3179,7 @@
|
|
|
2976
3179
|
.qde-dark {
|
|
2977
3180
|
background: #1e1e1e;
|
|
2978
3181
|
color: #e0e0e0;
|
|
3182
|
+
border-color: #444;
|
|
2979
3183
|
}
|
|
2980
3184
|
|
|
2981
3185
|
.qde-dark .qde-toolbar {
|
|
@@ -3007,6 +3211,20 @@
|
|
|
3007
3211
|
color: #e0e0e0;
|
|
3008
3212
|
}
|
|
3009
3213
|
|
|
3214
|
+
/* Dark mode code blocks */
|
|
3215
|
+
.qde-dark .qde-preview pre {
|
|
3216
|
+
background: #2d2d3a;
|
|
3217
|
+
color: #e6e6f0;
|
|
3218
|
+
}
|
|
3219
|
+
.qde-dark .qde-preview code {
|
|
3220
|
+
background: #2a2a3a;
|
|
3221
|
+
color: #e6e6f0;
|
|
3222
|
+
}
|
|
3223
|
+
.qde-dark .qde-preview pre code {
|
|
3224
|
+
background: transparent;
|
|
3225
|
+
color: inherit;
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3010
3228
|
/* Dark mode table styles */
|
|
3011
3229
|
.qde-dark .qde-preview table th,
|
|
3012
3230
|
.qde-dark .qde-preview table td {
|
|
@@ -3026,11 +3244,14 @@
|
|
|
3026
3244
|
.qde-mode-split .qde-editor {
|
|
3027
3245
|
flex-direction: column;
|
|
3028
3246
|
}
|
|
3029
|
-
|
|
3247
|
+
|
|
3030
3248
|
.qde-mode-split .qde-source {
|
|
3031
3249
|
border-right: none;
|
|
3032
3250
|
border-bottom: 1px solid #ddd;
|
|
3033
3251
|
}
|
|
3252
|
+
.qde-dark.qde-mode-split .qde-source {
|
|
3253
|
+
border-bottom-color: #444;
|
|
3254
|
+
}
|
|
3034
3255
|
}
|
|
3035
3256
|
`;
|
|
3036
3257
|
|
|
@@ -3177,24 +3398,34 @@
|
|
|
3177
3398
|
updateFromHTML() {
|
|
3178
3399
|
// Clone the preview panel to avoid modifying the actual DOM
|
|
3179
3400
|
const clonedPanel = this.previewPanel.cloneNode(true);
|
|
3180
|
-
|
|
3401
|
+
|
|
3181
3402
|
// Pre-process special elements on the clone
|
|
3182
3403
|
this.preprocessSpecialElements(clonedPanel);
|
|
3183
|
-
|
|
3404
|
+
|
|
3184
3405
|
this._html = this.previewPanel.innerHTML;
|
|
3185
|
-
|
|
3406
|
+
const newMarkdown = quikdown_bd.toMarkdown(clonedPanel, {
|
|
3186
3407
|
fence_plugin: this.createFencePlugin()
|
|
3187
3408
|
});
|
|
3188
|
-
|
|
3409
|
+
|
|
3410
|
+
// Push previous state to undo stack (now that we know the new markdown)
|
|
3411
|
+
if (!this._isUndoRedo) {
|
|
3412
|
+
this._pushUndoState(newMarkdown);
|
|
3413
|
+
}
|
|
3414
|
+
this._isUndoRedo = false;
|
|
3415
|
+
|
|
3416
|
+
this._markdown = newMarkdown;
|
|
3417
|
+
|
|
3189
3418
|
// Update source if visible
|
|
3190
3419
|
if (this.currentMode !== 'preview') {
|
|
3191
3420
|
this.sourceTextarea.value = this._markdown;
|
|
3192
3421
|
}
|
|
3193
|
-
|
|
3422
|
+
|
|
3194
3423
|
// Trigger change event
|
|
3195
3424
|
if (this.options.onChange) {
|
|
3196
3425
|
this.options.onChange(this._markdown, this._html);
|
|
3197
3426
|
}
|
|
3427
|
+
|
|
3428
|
+
this._updateUndoButtons();
|
|
3198
3429
|
}
|
|
3199
3430
|
|
|
3200
3431
|
/**
|
|
@@ -3791,18 +4022,12 @@
|
|
|
3791
4022
|
*/
|
|
3792
4023
|
renderSTL(code) {
|
|
3793
4024
|
const id = `qde-stl-viewer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
3794
|
-
|
|
3795
|
-
// Function to render the 3D model
|
|
4025
|
+
|
|
4026
|
+
// Function to render the 3D model (assumes window.THREE is loaded)
|
|
3796
4027
|
const render3D = () => {
|
|
3797
4028
|
const element = document.getElementById(id);
|
|
3798
4029
|
if (!element) return;
|
|
3799
|
-
|
|
3800
|
-
// Check if Three.js is available
|
|
3801
|
-
if (typeof window.THREE === 'undefined') {
|
|
3802
|
-
element.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">Three.js library not loaded. Add <script src="https://unpkg.com/three@0.147.0/build/three.min.js"></script> to your HTML.</div>';
|
|
3803
|
-
return;
|
|
3804
|
-
}
|
|
3805
|
-
|
|
4030
|
+
|
|
3806
4031
|
try {
|
|
3807
4032
|
const THREE = window.THREE;
|
|
3808
4033
|
|
|
@@ -3860,9 +4085,34 @@
|
|
|
3860
4085
|
}
|
|
3861
4086
|
};
|
|
3862
4087
|
|
|
3863
|
-
//
|
|
3864
|
-
|
|
3865
|
-
|
|
4088
|
+
// If Three.js is already loaded, render immediately. Otherwise lazy-load
|
|
4089
|
+
// it from a CDN (matches the GeoJSON/Leaflet pattern).
|
|
4090
|
+
if (window.THREE) {
|
|
4091
|
+
setTimeout(render3D, 0);
|
|
4092
|
+
} else {
|
|
4093
|
+
if (!window._qde_three_loading) {
|
|
4094
|
+
window._qde_three_loading = this.lazyLoadLibrary(
|
|
4095
|
+
'Three.js',
|
|
4096
|
+
() => window.THREE,
|
|
4097
|
+
'https://unpkg.com/three@0.147.0/build/three.min.js'
|
|
4098
|
+
).catch(_err => {
|
|
4099
|
+
console.warn('Failed to load Three.js for STL rendering');
|
|
4100
|
+
window._qde_three_loading = null;
|
|
4101
|
+
return false;
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
window._qde_three_loading.then(loaded => {
|
|
4105
|
+
if (loaded) {
|
|
4106
|
+
render3D();
|
|
4107
|
+
} else {
|
|
4108
|
+
const element = document.getElementById(id);
|
|
4109
|
+
if (element) {
|
|
4110
|
+
element.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">Failed to load Three.js for STL rendering</div>';
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
});
|
|
4114
|
+
}
|
|
4115
|
+
|
|
3866
4116
|
// Return placeholder with data-stl-id for copy functionality
|
|
3867
4117
|
return `<div id="${id}" class="qde-stl-container" data-stl-id="${id}" data-qd-fence="\`\`\`" data-qd-lang="stl" data-qd-source="${this.escapeHtml(code)}" contenteditable="false" style="height: 400px; background: #f0f0f0; display: flex; align-items: center; justify-content: center;">Loading 3D model...</div>`;
|
|
3868
4118
|
}
|
|
@@ -3954,30 +4204,64 @@
|
|
|
3954
4204
|
}
|
|
3955
4205
|
|
|
3956
4206
|
/**
|
|
3957
|
-
* Load plugins dynamically
|
|
4207
|
+
* Load plugins dynamically — honors both `plugins: { highlightjs, mermaid }`
|
|
4208
|
+
* (legacy) and the newer `preloadFences` option which can preload any
|
|
4209
|
+
* combination of fence libraries (or 'all') at construction time.
|
|
3958
4210
|
*/
|
|
3959
4211
|
async loadPlugins() {
|
|
3960
|
-
const
|
|
3961
|
-
|
|
3962
|
-
//
|
|
3963
|
-
if (this.options.plugins
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
this.loadCSS('https://unpkg.com/@highlightjs/cdn-assets/styles/github.min.css')
|
|
3967
|
-
);
|
|
4212
|
+
const namesToLoad = new Set();
|
|
4213
|
+
|
|
4214
|
+
// Legacy plugins option
|
|
4215
|
+
if (this.options.plugins) {
|
|
4216
|
+
if (this.options.plugins.highlightjs) namesToLoad.add('highlightjs');
|
|
4217
|
+
if (this.options.plugins.mermaid) namesToLoad.add('mermaid');
|
|
3968
4218
|
}
|
|
3969
|
-
|
|
3970
|
-
//
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
4219
|
+
|
|
4220
|
+
// New preloadFences option
|
|
4221
|
+
const pf = this.options.preloadFences;
|
|
4222
|
+
if (pf === 'all') {
|
|
4223
|
+
Object.keys(FENCE_LIBRARIES).forEach(n => namesToLoad.add(n));
|
|
4224
|
+
} else if (Array.isArray(pf)) {
|
|
4225
|
+
for (const entry of pf) {
|
|
4226
|
+
if (typeof entry === 'string') {
|
|
4227
|
+
if (FENCE_LIBRARIES[entry]) namesToLoad.add(entry);
|
|
4228
|
+
else console.warn(`QuikdownEditor: unknown preloadFences entry "${entry}"`);
|
|
4229
|
+
} else if (entry && typeof entry === 'object' && entry.script) {
|
|
4230
|
+
// Custom library: { name, script, css? }
|
|
4231
|
+
namesToLoad.add('__custom__:' + (entry.name || entry.script));
|
|
4232
|
+
FENCE_LIBRARIES['__custom__:' + (entry.name || entry.script)] = {
|
|
4233
|
+
check: () => false,
|
|
4234
|
+
script: entry.script,
|
|
4235
|
+
css: entry.css
|
|
4236
|
+
};
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
} else if (pf) {
|
|
4240
|
+
console.warn('QuikdownEditor: preloadFences should be "all", an array, or null');
|
|
3979
4241
|
}
|
|
3980
|
-
|
|
4242
|
+
|
|
4243
|
+
// Load each in parallel; respect already-loaded state
|
|
4244
|
+
const promises = [];
|
|
4245
|
+
for (const name of namesToLoad) {
|
|
4246
|
+
const lib = FENCE_LIBRARIES[name];
|
|
4247
|
+
if (!lib || lib.check()) continue;
|
|
4248
|
+
if (lib.beforeLoad) lib.beforeLoad();
|
|
4249
|
+
const p = (async () => {
|
|
4250
|
+
try {
|
|
4251
|
+
const tasks = [];
|
|
4252
|
+
if (lib.script) tasks.push(this.loadScript(lib.script));
|
|
4253
|
+
if (lib.css) tasks.push(this.loadCSS(lib.css, 'qde-hljs-light'));
|
|
4254
|
+
if (lib.cssDark) tasks.push(this.loadCSS(lib.cssDark, 'qde-hljs-dark'));
|
|
4255
|
+
await Promise.all(tasks);
|
|
4256
|
+
if (lib.css && lib.cssDark) this._syncHljsTheme();
|
|
4257
|
+
if (lib.afterLoad) lib.afterLoad();
|
|
4258
|
+
} catch (err) {
|
|
4259
|
+
console.warn(`QuikdownEditor: failed to preload ${name}:`, err);
|
|
4260
|
+
}
|
|
4261
|
+
})();
|
|
4262
|
+
promises.push(p);
|
|
4263
|
+
}
|
|
4264
|
+
|
|
3981
4265
|
await Promise.all(promises);
|
|
3982
4266
|
}
|
|
3983
4267
|
|
|
@@ -4029,18 +4313,31 @@
|
|
|
4029
4313
|
/**
|
|
4030
4314
|
* Load external CSS
|
|
4031
4315
|
*/
|
|
4032
|
-
loadCSS(href) {
|
|
4316
|
+
loadCSS(href, id) {
|
|
4033
4317
|
return new Promise((resolve) => {
|
|
4034
4318
|
const link = document.createElement('link');
|
|
4035
4319
|
link.rel = 'stylesheet';
|
|
4036
4320
|
link.href = href;
|
|
4321
|
+
if (id) link.id = id;
|
|
4037
4322
|
link.onload = resolve;
|
|
4038
4323
|
document.head.appendChild(link);
|
|
4039
4324
|
// Resolve anyway after timeout (CSS doesn't always fire onload)
|
|
4040
4325
|
setTimeout(resolve, 1000);
|
|
4041
4326
|
});
|
|
4042
4327
|
}
|
|
4043
|
-
|
|
4328
|
+
|
|
4329
|
+
/**
|
|
4330
|
+
* Enable the hljs stylesheet matching the current theme and disable
|
|
4331
|
+
* the other one. Called from applyTheme and after hljs CSS loads.
|
|
4332
|
+
*/
|
|
4333
|
+
_syncHljsTheme() {
|
|
4334
|
+
const isDark = this.container.classList.contains('qde-dark');
|
|
4335
|
+
const light = document.getElementById('qde-hljs-light');
|
|
4336
|
+
const dark = document.getElementById('qde-hljs-dark');
|
|
4337
|
+
if (light) light.disabled = isDark;
|
|
4338
|
+
if (dark) dark.disabled = !isDark;
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4044
4341
|
/**
|
|
4045
4342
|
* Apply the current theme (based on this.options.theme)
|
|
4046
4343
|
*/
|
|
@@ -4058,11 +4355,13 @@
|
|
|
4058
4355
|
this.container.classList.toggle('qde-dark', mq.matches);
|
|
4059
4356
|
this._autoThemeListener = (e) => {
|
|
4060
4357
|
this.container.classList.toggle('qde-dark', e.matches);
|
|
4358
|
+
this._syncHljsTheme();
|
|
4061
4359
|
};
|
|
4062
4360
|
mq.addEventListener('change', this._autoThemeListener);
|
|
4063
4361
|
} else {
|
|
4064
4362
|
this.container.classList.toggle('qde-dark', theme === 'dark');
|
|
4065
4363
|
}
|
|
4364
|
+
this._syncHljsTheme();
|
|
4066
4365
|
}
|
|
4067
4366
|
|
|
4068
4367
|
/**
|
|
@@ -4124,10 +4423,18 @@
|
|
|
4124
4423
|
*/
|
|
4125
4424
|
setMode(mode) {
|
|
4126
4425
|
if (!['source', 'preview', 'split'].includes(mode)) return;
|
|
4127
|
-
|
|
4426
|
+
|
|
4427
|
+
// Preserve theme class across mode swap (the assignment to className
|
|
4428
|
+
// below would otherwise wipe it out — this used to be a no-op bug
|
|
4429
|
+
// where dark mode was lost on every setMode call).
|
|
4430
|
+
const wasDark = this.container.classList.contains('qde-dark');
|
|
4431
|
+
|
|
4128
4432
|
this.currentMode = mode;
|
|
4129
4433
|
this.container.className = `qde-container qde-mode-${mode}`;
|
|
4130
|
-
|
|
4434
|
+
if (wasDark) {
|
|
4435
|
+
this.container.classList.add('qde-dark');
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4131
4438
|
// Update toolbar buttons
|
|
4132
4439
|
if (this.toolbar) {
|
|
4133
4440
|
this.toolbar.querySelectorAll('.qde-btn[data-mode]').forEach(btn => {
|
|
@@ -4135,11 +4442,6 @@
|
|
|
4135
4442
|
});
|
|
4136
4443
|
}
|
|
4137
4444
|
|
|
4138
|
-
// Apply theme class
|
|
4139
|
-
if (this.container.classList.contains('qde-dark')) {
|
|
4140
|
-
this.container.classList.add('qde-dark');
|
|
4141
|
-
}
|
|
4142
|
-
|
|
4143
4445
|
// Make fence blocks non-editable when showing preview
|
|
4144
4446
|
if (mode !== 'source') {
|
|
4145
4447
|
setTimeout(() => this.makeFencesNonEditable(), 0);
|
|
@@ -4485,95 +4787,140 @@
|
|
|
4485
4787
|
* @returns {string} markdown with lazy linefeeds resolved
|
|
4486
4788
|
*/
|
|
4487
4789
|
static convertLazyLinefeeds(markdown) {
|
|
4488
|
-
|
|
4489
|
-
|
|
4790
|
+
// Two-phase approach (much cleaner than the old single pass):
|
|
4791
|
+
//
|
|
4792
|
+
// Phase A: walk lines, classify each as { content, blank, fence }.
|
|
4793
|
+
// Inside fences, lines are passed through verbatim.
|
|
4794
|
+
// Phase B: emit lines with the rule:
|
|
4795
|
+
// "between two adjacent CONTENT lines, ensure exactly one
|
|
4796
|
+
// blank line — never zero, never more than one."
|
|
4797
|
+
//
|
|
4798
|
+
// The rule applies regardless of whether the content lines are
|
|
4799
|
+
// headings, lists, blockquotes, table rows, paragraphs, or HR — any
|
|
4800
|
+
// adjacent pair of non-fence non-blank lines gets exactly one blank
|
|
4801
|
+
// between them. This produces the cleanest possible output for any
|
|
4802
|
+
// input and is fully idempotent.
|
|
4803
|
+
//
|
|
4804
|
+
// Lines that are whitespace-only (e.g. " ") are normalized to
|
|
4805
|
+
// empty strings, eliminating "phantom" blank lines.
|
|
4806
|
+
//
|
|
4807
|
+
// Lists are a special case: adjacent list items (same marker type)
|
|
4808
|
+
// should NOT get a blank line between them, otherwise we'd break
|
|
4809
|
+
// tight lists.
|
|
4810
|
+
//
|
|
4811
|
+
// Same applies to blockquote lines and table rows — adjacent rows
|
|
4812
|
+
// belong to the same block.
|
|
4813
|
+
|
|
4814
|
+
const inputLines = (markdown || '').split('\n');
|
|
4815
|
+
|
|
4816
|
+
// -------- Phase A: classify lines, normalize whitespace-only --------
|
|
4817
|
+
// Each entry: { line, kind } where kind is one of:
|
|
4818
|
+
// 'fence-open', 'fence-close', 'fence-body', 'blank', 'content'
|
|
4819
|
+
// Plus a 'category' for content lines: 'list-ul', 'list-ol',
|
|
4820
|
+
// 'blockquote', 'table', 'heading', 'hr', 'paragraph'
|
|
4821
|
+
const items = [];
|
|
4490
4822
|
let inFence = false;
|
|
4491
4823
|
let fenceChar = null;
|
|
4492
4824
|
let fenceLen = 0;
|
|
4493
|
-
let inHTMLBlock = false;
|
|
4494
4825
|
|
|
4495
|
-
for (
|
|
4496
|
-
const line =
|
|
4826
|
+
for (const rawLine of inputLines) {
|
|
4827
|
+
const line = rawLine;
|
|
4497
4828
|
const trimmed = line.trim();
|
|
4498
4829
|
|
|
4499
|
-
//
|
|
4830
|
+
// Fence tracking
|
|
4500
4831
|
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
4501
|
-
if (fenceMatch) {
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4832
|
+
if (fenceMatch && !inFence) {
|
|
4833
|
+
inFence = true;
|
|
4834
|
+
fenceChar = fenceMatch[1][0];
|
|
4835
|
+
fenceLen = fenceMatch[1].length;
|
|
4836
|
+
items.push({ line, kind: 'fence-open' });
|
|
4837
|
+
continue;
|
|
4838
|
+
}
|
|
4839
|
+
if (inFence) {
|
|
4840
|
+
if (fenceMatch &&
|
|
4841
|
+
fenceMatch[1][0] === fenceChar &&
|
|
4842
|
+
fenceMatch[1].length >= fenceLen &&
|
|
4843
|
+
/^(`{3,}|~{3,})\s*$/.test(trimmed)) {
|
|
4511
4844
|
inFence = false;
|
|
4512
4845
|
fenceChar = null;
|
|
4513
4846
|
fenceLen = 0;
|
|
4514
|
-
|
|
4515
|
-
|
|
4847
|
+
items.push({ line, kind: 'fence-close' });
|
|
4848
|
+
} else {
|
|
4849
|
+
items.push({ line, kind: 'fence-body' });
|
|
4516
4850
|
}
|
|
4851
|
+
continue;
|
|
4517
4852
|
}
|
|
4518
4853
|
|
|
4519
|
-
//
|
|
4520
|
-
if (
|
|
4521
|
-
|
|
4854
|
+
// Outside fence: whitespace-only lines become canonical blanks
|
|
4855
|
+
if (trimmed === '') {
|
|
4856
|
+
items.push({ line: '', kind: 'blank' });
|
|
4522
4857
|
continue;
|
|
4523
4858
|
}
|
|
4524
4859
|
|
|
4525
|
-
//
|
|
4526
|
-
|
|
4527
|
-
if (
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4860
|
+
// Categorize content lines so we can recognize adjacent same-kind blocks
|
|
4861
|
+
let category = 'paragraph';
|
|
4862
|
+
if (/^#{1,6}\s/.test(trimmed)) category = 'heading';
|
|
4863
|
+
else if (/^[-_*](\s*[-_*]){2,}\s*$/.test(trimmed)) category = 'hr';
|
|
4864
|
+
else if (/^(\d+\.)\s/.test(trimmed)) category = 'list-ol';
|
|
4865
|
+
else if (/^[-*+]\s/.test(trimmed)) category = 'list-ul';
|
|
4866
|
+
else if (/^>/.test(trimmed)) category = 'blockquote';
|
|
4867
|
+
else if (/^\|/.test(trimmed)) category = 'table';
|
|
4868
|
+
// Indented continuation of a list (2+ leading spaces or tab)
|
|
4869
|
+
else if (/^(?: {4}|\t| {2,}[-*+]| {2,}\d+\.)/.test(line)) category = 'list-cont';
|
|
4870
|
+
|
|
4871
|
+
items.push({ line, kind: 'content', category });
|
|
4872
|
+
}
|
|
4873
|
+
|
|
4874
|
+
// -------- Phase B: emit with exactly-one-blank-line normalization --------
|
|
4875
|
+
// Same-block adjacent lines (lists, blockquotes, tables) stay
|
|
4876
|
+
// touching; any other adjacent content pair gets exactly one blank.
|
|
4877
|
+
const result = [];
|
|
4878
|
+
let prev = null; // last emitted non-blank content item
|
|
4879
|
+
|
|
4880
|
+
function inSameBlock(a, b) {
|
|
4881
|
+
if (!a || !b) return false;
|
|
4882
|
+
// Lists: same marker family OR list-content continuation
|
|
4883
|
+
if ((a.category === 'list-ul' || a.category === 'list-ol' || a.category === 'list-cont') &&
|
|
4884
|
+
(b.category === 'list-ul' || b.category === 'list-ol' || b.category === 'list-cont')) {
|
|
4885
|
+
return true;
|
|
4531
4886
|
}
|
|
4887
|
+
// Blockquotes
|
|
4888
|
+
if (a.category === 'blockquote' && b.category === 'blockquote') return true;
|
|
4889
|
+
// Table rows
|
|
4890
|
+
if (a.category === 'table' && b.category === 'table') return true;
|
|
4891
|
+
return false;
|
|
4892
|
+
}
|
|
4532
4893
|
|
|
4533
|
-
|
|
4534
|
-
if (
|
|
4535
|
-
//
|
|
4536
|
-
if (result.length
|
|
4537
|
-
result.push(
|
|
4894
|
+
for (const item of items) {
|
|
4895
|
+
if (item.kind === 'fence-open' || item.kind === 'fence-body' || item.kind === 'fence-close') {
|
|
4896
|
+
// Fences: ensure exactly one blank line before the fence-open
|
|
4897
|
+
if (item.kind === 'fence-open' && prev && result.length > 0 && result[result.length - 1] !== '') {
|
|
4898
|
+
result.push('');
|
|
4538
4899
|
}
|
|
4900
|
+
result.push(item.line);
|
|
4901
|
+
if (item.kind === 'fence-close') prev = { kind: 'content', category: 'fence' };
|
|
4539
4902
|
continue;
|
|
4540
4903
|
}
|
|
4541
4904
|
|
|
4542
|
-
|
|
4543
|
-
|
|
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);
|
|
4905
|
+
if (item.kind === 'blank') {
|
|
4906
|
+
// Skip — Phase B inserts its own blank lines as needed
|
|
4553
4907
|
continue;
|
|
4554
4908
|
}
|
|
4555
4909
|
|
|
4556
|
-
//
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
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('');
|
|
4910
|
+
// item.kind === 'content'
|
|
4911
|
+
if (prev) {
|
|
4912
|
+
if (inSameBlock(prev, item)) ; else {
|
|
4913
|
+
// Different blocks (or paragraphs): exactly one blank
|
|
4914
|
+
if (result[result.length - 1] !== '') result.push('');
|
|
4571
4915
|
}
|
|
4572
4916
|
}
|
|
4573
|
-
|
|
4574
|
-
|
|
4917
|
+
result.push(item.line);
|
|
4918
|
+
prev = item;
|
|
4575
4919
|
}
|
|
4576
4920
|
|
|
4921
|
+
// Trim trailing blank lines so output has exactly one terminal newline
|
|
4922
|
+
while (result.length > 0 && result[result.length - 1] === '') result.pop();
|
|
4923
|
+
|
|
4577
4924
|
return result.join('\n');
|
|
4578
4925
|
}
|
|
4579
4926
|
|