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