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