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