quikdown 1.0.4 → 1.0.5
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 +118 -499
- package/dist/quikdown.cjs +105 -41
- package/dist/quikdown.d.ts +14 -0
- package/dist/quikdown.dark.css +1 -1
- package/dist/quikdown.esm.js +105 -41
- package/dist/quikdown.esm.min.js +2 -2
- package/dist/quikdown.esm.min.js.map +1 -1
- package/dist/quikdown.light.css +1 -1
- package/dist/quikdown.umd.js +105 -41
- package/dist/quikdown.umd.min.js +2 -2
- package/dist/quikdown.umd.min.js.map +1 -1
- package/dist/quikdown_bd.cjs +562 -258
- package/dist/quikdown_bd.d.ts +45 -13
- package/dist/quikdown_bd.esm.js +562 -258
- package/dist/quikdown_bd.esm.min.js +2 -2
- package/dist/quikdown_bd.esm.min.js.map +1 -1
- package/dist/quikdown_bd.umd.js +562 -258
- package/dist/quikdown_bd.umd.min.js +2 -2
- package/dist/quikdown_bd.umd.min.js.map +1 -1
- package/dist/quikdown_edit.cjs +2319 -0
- package/dist/quikdown_edit.d.ts +195 -0
- package/dist/quikdown_edit.esm.js +2317 -0
- package/dist/quikdown_edit.esm.min.js +14 -0
- package/dist/quikdown_edit.esm.min.js.map +1 -0
- package/dist/quikdown_edit.umd.js +2325 -0
- package/dist/quikdown_edit.umd.min.js +14 -0
- package/dist/quikdown_edit.umd.min.js.map +1 -0
- package/package.json +79 -9
- package/dist/quikdown-lex.cjs +0 -810
- package/dist/quikdown-lex.esm.js +0 -808
- package/dist/quikdown-lex.esm.min.js +0 -8
- package/dist/quikdown-lex.esm.min.js.map +0 -1
- package/dist/quikdown-lex.umd.js +0 -816
- package/dist/quikdown-lex.umd.min.js +0 -8
- package/dist/quikdown-lex.umd.min.js.map +0 -1
package/dist/quikdown_bd.esm.js
CHANGED
|
@@ -1,275 +1,365 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* quikdown_bd - Bidirectional Markdown Parser
|
|
3
|
-
* @version 1.0.
|
|
3
|
+
* @version 1.0.5
|
|
4
4
|
* @license BSD-2-Clause
|
|
5
5
|
* @copyright DeftIO 2025
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* quikdown - A minimal markdown parser optimized for chat/LLM output
|
|
9
|
+
* Supports tables, code blocks, lists, and common formatting
|
|
10
|
+
* @param {string} markdown - The markdown source text
|
|
11
|
+
* @param {Object} options - Optional configuration object
|
|
12
|
+
* @param {Function} options.fence_plugin - Custom renderer for fenced code blocks
|
|
13
|
+
* (content, fence_string) => html string
|
|
14
|
+
* @param {boolean} options.inline_styles - If true, uses inline styles instead of classes
|
|
15
|
+
* @param {boolean} options.bidirectional - If true, adds data-qd attributes for source tracking
|
|
16
|
+
* @param {boolean} options.lazy_linefeeds - If true, single newlines become <br> tags
|
|
17
|
+
* @returns {string} - The rendered HTML
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
|
-
// Version
|
|
16
|
-
const
|
|
20
|
+
// Version will be injected at build time
|
|
21
|
+
const quikdownVersion = '1.0.5';
|
|
17
22
|
|
|
18
|
-
//
|
|
23
|
+
// Constants for reuse
|
|
24
|
+
const CLASS_PREFIX = 'quikdown-';
|
|
25
|
+
const PLACEHOLDER_CB = '§CB';
|
|
26
|
+
const PLACEHOLDER_IC = '§IC';
|
|
27
|
+
|
|
28
|
+
// Escape map at module level
|
|
19
29
|
const ESC_MAP = {'&':'&','<':'<','>':'>','"':'"',"'":'''};
|
|
20
|
-
function escapeHtml(text) {
|
|
21
|
-
return text.replace(/[&<>"']/g, m => ESC_MAP[m]);
|
|
22
|
-
}
|
|
23
30
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
// Single source of truth for all style definitions - optimized
|
|
32
|
+
const QUIKDOWN_STYLES = {
|
|
33
|
+
h1: 'font-size:2em;font-weight:600;margin:.67em 0;text-align:left',
|
|
34
|
+
h2: 'font-size:1.5em;font-weight:600;margin:.83em 0',
|
|
35
|
+
h3: 'font-size:1.25em;font-weight:600;margin:1em 0',
|
|
36
|
+
h4: 'font-size:1em;font-weight:600;margin:1.33em 0',
|
|
37
|
+
h5: 'font-size:.875em;font-weight:600;margin:1.67em 0',
|
|
38
|
+
h6: 'font-size:.85em;font-weight:600;margin:2em 0',
|
|
39
|
+
pre: 'background:#f4f4f4;padding:10px;border-radius:4px;overflow-x:auto;margin:1em 0',
|
|
40
|
+
code: 'background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace',
|
|
41
|
+
blockquote: 'border-left:4px solid #ddd;margin-left:0;padding-left:1em',
|
|
42
|
+
table: 'border-collapse:collapse;width:100%;margin:1em 0',
|
|
43
|
+
th: 'border:1px solid #ddd;padding:8px;background-color:#f2f2f2;font-weight:bold;text-align:left',
|
|
44
|
+
td: 'border:1px solid #ddd;padding:8px;text-align:left',
|
|
45
|
+
hr: 'border:none;border-top:1px solid #ddd;margin:1em 0',
|
|
46
|
+
img: 'max-width:100%;height:auto',
|
|
47
|
+
a: 'color:#06c;text-decoration:underline',
|
|
48
|
+
strong: 'font-weight:bold',
|
|
49
|
+
em: 'font-style:italic',
|
|
50
|
+
del: 'text-decoration:line-through',
|
|
51
|
+
ul: 'margin:.5em 0;padding-left:2em',
|
|
52
|
+
ol: 'margin:.5em 0;padding-left:2em',
|
|
53
|
+
li: 'margin:.25em 0',
|
|
54
|
+
// Task list specific styles
|
|
55
|
+
'task-item': 'list-style:none',
|
|
56
|
+
'task-checkbox': 'margin-right:.5em'
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Factory function to create getAttr for a given context
|
|
60
|
+
function createGetAttr(inline_styles, styles) {
|
|
61
|
+
return function(tag, additionalStyle = '') {
|
|
35
62
|
if (inline_styles) {
|
|
36
|
-
|
|
37
|
-
if (style
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
let style = styles[tag];
|
|
64
|
+
if (!style && !additionalStyle) return '';
|
|
65
|
+
|
|
66
|
+
// Remove default text-align if we're adding a different alignment
|
|
67
|
+
if (additionalStyle && additionalStyle.includes('text-align') && style && style.includes('text-align')) {
|
|
68
|
+
style = style.replace(/text-align:[^;]+;?/, '').trim();
|
|
69
|
+
if (style && !style.endsWith(';')) style += ';';
|
|
40
70
|
}
|
|
71
|
+
|
|
72
|
+
/* istanbul ignore next - defensive: additionalStyle without style doesn't occur with current tags */
|
|
73
|
+
const fullStyle = additionalStyle ? (style ? `${style}${additionalStyle}` : additionalStyle) : style;
|
|
74
|
+
return ` style="${fullStyle}"`;
|
|
41
75
|
} else {
|
|
42
|
-
|
|
76
|
+
const classAttr = ` class="${CLASS_PREFIX}${tag}"`;
|
|
77
|
+
// Apply inline styles for alignment even when using CSS classes
|
|
78
|
+
if (additionalStyle) {
|
|
79
|
+
return `${classAttr} style="${additionalStyle}"`;
|
|
80
|
+
}
|
|
81
|
+
return classAttr;
|
|
43
82
|
}
|
|
44
|
-
|
|
45
|
-
return attrs;
|
|
46
83
|
};
|
|
47
84
|
}
|
|
48
85
|
|
|
49
|
-
|
|
50
|
-
* Enhanced markdown parser with bidirectional support
|
|
51
|
-
* Wraps the core parser and adds data-qd attributes
|
|
52
|
-
*/
|
|
53
|
-
function quikdown_bd(markdown, options = {}) {
|
|
86
|
+
function quikdown(markdown, options = {}) {
|
|
54
87
|
if (!markdown || typeof markdown !== 'string') {
|
|
55
88
|
return '';
|
|
56
89
|
}
|
|
57
90
|
|
|
58
|
-
const { fence_plugin, inline_styles = false, bidirectional =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
91
|
+
const { fence_plugin, inline_styles = false, bidirectional = false, lazy_linefeeds = false } = options;
|
|
92
|
+
const styles = QUIKDOWN_STYLES; // Use module-level styles
|
|
93
|
+
const getAttr = createGetAttr(inline_styles, styles); // Create getAttr once
|
|
94
|
+
|
|
95
|
+
// Escape HTML entities to prevent XSS
|
|
96
|
+
function escapeHtml(text) {
|
|
97
|
+
return text.replace(/[&<>"']/g, m => ESC_MAP[m]);
|
|
64
98
|
}
|
|
65
99
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const QUIKDOWN_STYLES = {
|
|
70
|
-
h1: 'font-size:2em;font-weight:600;margin:.67em 0;text-align:left',
|
|
71
|
-
h2: 'font-size:1.5em;font-weight:600;margin:.83em 0',
|
|
72
|
-
h3: 'font-size:1.25em;font-weight:600;margin:1em 0',
|
|
73
|
-
h4: 'font-size:1em;font-weight:600;margin:1.33em 0',
|
|
74
|
-
h5: 'font-size:.875em;font-weight:600;margin:1.67em 0',
|
|
75
|
-
h6: 'font-size:.85em;font-weight:600;margin:2em 0',
|
|
76
|
-
pre: 'background:#f4f4f4;padding:10px;border-radius:4px;overflow-x:auto;margin:1em 0',
|
|
77
|
-
code: 'background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace',
|
|
78
|
-
blockquote: 'border-left:4px solid #ddd;margin-left:0;padding-left:1em',
|
|
79
|
-
table: 'border-collapse:collapse;width:100%;margin:1em 0',
|
|
80
|
-
th: 'border:1px solid #ddd;padding:8px;background-color:#f2f2f2;font-weight:bold;text-align:left',
|
|
81
|
-
td: 'border:1px solid #ddd;padding:8px;text-align:left',
|
|
82
|
-
hr: 'border:none;border-top:1px solid #ddd;margin:1em 0',
|
|
83
|
-
img: 'max-width:100%;height:auto',
|
|
84
|
-
a: 'color:#06c;text-decoration:underline',
|
|
85
|
-
strong: 'font-weight:bold',
|
|
86
|
-
em: 'font-style:italic',
|
|
87
|
-
del: 'text-decoration:line-through',
|
|
88
|
-
ul: 'margin:.5em 0;padding-left:2em',
|
|
89
|
-
ol: 'margin:.5em 0;padding-left:2em',
|
|
90
|
-
li: 'margin:.25em 0',
|
|
91
|
-
'task-item': 'list-style:none',
|
|
92
|
-
'task-checkbox': 'margin-right:.5em'
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const getAttr = createGetAttrBD(inline_styles, QUIKDOWN_STYLES);
|
|
100
|
+
// Helper to add data-qd attributes for bidirectional support
|
|
101
|
+
const dataQd = bidirectional ? (marker) => ` data-qd="${escapeHtml(marker)}"` : () => '';
|
|
96
102
|
|
|
97
|
-
//
|
|
103
|
+
// Sanitize URLs to prevent XSS attacks
|
|
104
|
+
function sanitizeUrl(url, allowUnsafe = false) {
|
|
105
|
+
/* istanbul ignore next - defensive programming, regex ensures url is never empty */
|
|
106
|
+
if (!url) return '';
|
|
107
|
+
|
|
108
|
+
// If unsafe URLs are explicitly allowed, return as-is
|
|
109
|
+
if (allowUnsafe) return url;
|
|
110
|
+
|
|
111
|
+
const trimmedUrl = url.trim();
|
|
112
|
+
const lowerUrl = trimmedUrl.toLowerCase();
|
|
113
|
+
|
|
114
|
+
// Block dangerous protocols
|
|
115
|
+
const dangerousProtocols = ['javascript:', 'vbscript:', 'data:'];
|
|
116
|
+
|
|
117
|
+
for (const protocol of dangerousProtocols) {
|
|
118
|
+
if (lowerUrl.startsWith(protocol)) {
|
|
119
|
+
// Exception: Allow data:image/* for images
|
|
120
|
+
if (protocol === 'data:' && lowerUrl.startsWith('data:image/')) {
|
|
121
|
+
return trimmedUrl;
|
|
122
|
+
}
|
|
123
|
+
// Return safe empty link for dangerous protocols
|
|
124
|
+
return '#';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return trimmedUrl;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Process the markdown in phases
|
|
98
132
|
let html = markdown;
|
|
99
133
|
|
|
100
|
-
// Phase 1: Extract and protect code blocks
|
|
134
|
+
// Phase 1: Extract and protect code blocks and inline code
|
|
101
135
|
const codeBlocks = [];
|
|
102
136
|
const inlineCodes = [];
|
|
103
137
|
|
|
104
|
-
// Extract fenced code blocks
|
|
138
|
+
// Extract fenced code blocks first (supports both ``` and ~~~)
|
|
139
|
+
// Match paired fences - ``` with ``` and ~~~ with ~~~
|
|
140
|
+
// Fence must be at start of line
|
|
105
141
|
html = html.replace(/^(```|~~~)([^\n]*)\n([\s\S]*?)^\1$/gm, (match, fence, lang, code) => {
|
|
106
|
-
const placeholder =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
142
|
+
const placeholder = `${PLACEHOLDER_CB}${codeBlocks.length}§`;
|
|
143
|
+
|
|
144
|
+
// Trim the language specification
|
|
145
|
+
const langTrimmed = lang ? lang.trim() : '';
|
|
146
|
+
|
|
147
|
+
// If custom fence plugin is provided, use it
|
|
148
|
+
if (fence_plugin && typeof fence_plugin === 'function') {
|
|
149
|
+
codeBlocks.push({
|
|
150
|
+
lang: langTrimmed,
|
|
151
|
+
code: code.trimEnd(),
|
|
152
|
+
custom: true,
|
|
153
|
+
fence: fence
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
codeBlocks.push({
|
|
157
|
+
lang: langTrimmed,
|
|
158
|
+
code: escapeHtml(code.trimEnd()),
|
|
159
|
+
custom: false,
|
|
160
|
+
fence: fence
|
|
161
|
+
});
|
|
162
|
+
}
|
|
113
163
|
return placeholder;
|
|
114
164
|
});
|
|
115
165
|
|
|
116
166
|
// Extract inline code
|
|
117
167
|
html = html.replace(/`([^`]+)`/g, (match, code) => {
|
|
118
|
-
const placeholder =
|
|
119
|
-
inlineCodes.push(
|
|
120
|
-
code: escapeHtml(code),
|
|
121
|
-
original: match
|
|
122
|
-
});
|
|
168
|
+
const placeholder = `${PLACEHOLDER_IC}${inlineCodes.length}§`;
|
|
169
|
+
inlineCodes.push(escapeHtml(code));
|
|
123
170
|
return placeholder;
|
|
124
171
|
});
|
|
125
172
|
|
|
126
|
-
//
|
|
173
|
+
// Now escape HTML in the rest of the content
|
|
127
174
|
html = escapeHtml(html);
|
|
128
175
|
|
|
129
|
-
//
|
|
176
|
+
// Phase 2: Process block elements
|
|
177
|
+
|
|
178
|
+
// Process tables
|
|
179
|
+
html = processTable(html, getAttr);
|
|
180
|
+
|
|
181
|
+
// Process headings (supports optional trailing #'s)
|
|
130
182
|
html = html.replace(/^(#{1,6})\s+(.+?)\s*#*$/gm, (match, hashes, content) => {
|
|
131
183
|
const level = hashes.length;
|
|
132
|
-
|
|
133
|
-
return `<h${level}${getAttr('h' + level, '', sourceMarker)}>${content}</h${level}>`;
|
|
184
|
+
return `<h${level}${getAttr('h' + level)}${dataQd(hashes)}>${content}</h${level}>`;
|
|
134
185
|
});
|
|
135
186
|
|
|
136
|
-
// Process
|
|
137
|
-
html = html.replace(
|
|
138
|
-
|
|
139
|
-
html = html.replace(
|
|
140
|
-
html = html.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, `<em${getAttr('em', '', '_')}>$1</em>`);
|
|
141
|
-
html = html.replace(/~~(.+?)~~/g, `<del${getAttr('del', '', '~~')}>$1</del>`);
|
|
142
|
-
|
|
143
|
-
// Process blockquotes
|
|
144
|
-
html = html.replace(/^>\s+(.+)$/gm, `<blockquote${getAttr('blockquote', '', '>')}>$1</blockquote>`);
|
|
145
|
-
html = html.replace(/<\/blockquote>\n<blockquote[^>]*>/g, '\n');
|
|
187
|
+
// Process blockquotes (must handle escaped > since we already escaped HTML)
|
|
188
|
+
html = html.replace(/^>\s+(.+)$/gm, `<blockquote${getAttr('blockquote')}>$1</blockquote>`);
|
|
189
|
+
// Merge consecutive blockquotes
|
|
190
|
+
html = html.replace(/<\/blockquote>\n<blockquote>/g, '\n');
|
|
146
191
|
|
|
147
192
|
// Process horizontal rules
|
|
148
|
-
html = html.replace(/^---+$/gm, `<hr${getAttr('hr'
|
|
193
|
+
html = html.replace(/^---+$/gm, `<hr${getAttr('hr')}>`);
|
|
149
194
|
|
|
150
|
-
// Process lists
|
|
151
|
-
html =
|
|
195
|
+
// Process lists
|
|
196
|
+
html = processLists(html, getAttr, inline_styles, bidirectional);
|
|
152
197
|
|
|
153
|
-
// Process
|
|
198
|
+
// Phase 3: Process inline elements
|
|
199
|
+
|
|
200
|
+
// Images (must come before links, with URL sanitization)
|
|
154
201
|
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
|
155
|
-
|
|
202
|
+
const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);
|
|
203
|
+
const altAttr = bidirectional && alt ? ` data-qd-alt="${escapeHtml(alt)}"` : '';
|
|
204
|
+
const srcAttr = bidirectional ? ` data-qd-src="${escapeHtml(src)}"` : '';
|
|
205
|
+
return `<img${getAttr('img')} src="${sanitizedSrc}" alt="${alt}"${altAttr}${srcAttr}${dataQd('!')}>`;
|
|
156
206
|
});
|
|
157
207
|
|
|
208
|
+
// Links (with URL sanitization)
|
|
158
209
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
|
|
159
|
-
|
|
210
|
+
// Sanitize URL to prevent XSS
|
|
211
|
+
const sanitizedHref = sanitizeUrl(href, options.allow_unsafe_urls);
|
|
212
|
+
const isExternal = /^https?:\/\//i.test(sanitizedHref);
|
|
213
|
+
const rel = isExternal ? ' rel="noopener noreferrer"' : '';
|
|
214
|
+
const textAttr = bidirectional ? ` data-qd-text="${escapeHtml(text)}"` : '';
|
|
215
|
+
return `<a${getAttr('a')} href="${sanitizedHref}"${rel}${textAttr}${dataQd('[')}>${text}</a>`;
|
|
160
216
|
});
|
|
161
217
|
|
|
162
|
-
//
|
|
163
|
-
html =
|
|
218
|
+
// Autolinks - convert bare URLs to clickable links
|
|
219
|
+
html = html.replace(/(^|\s)(https?:\/\/[^\s<]+)/g, (match, prefix, url) => {
|
|
220
|
+
const sanitizedUrl = sanitizeUrl(url, options.allow_unsafe_urls);
|
|
221
|
+
return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
|
|
222
|
+
});
|
|
164
223
|
|
|
165
|
-
//
|
|
166
|
-
|
|
224
|
+
// Process inline formatting (bold, italic, strikethrough)
|
|
225
|
+
const inlinePatterns = [
|
|
226
|
+
[/\*\*(.+?)\*\*/g, 'strong', '**'],
|
|
227
|
+
[/__(.+?)__/g, 'strong', '__'],
|
|
228
|
+
[/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, 'em', '*'],
|
|
229
|
+
[/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em', '_'],
|
|
230
|
+
[/~~(.+?)~~/g, 'del', '~~']
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
inlinePatterns.forEach(([pattern, tag, marker]) => {
|
|
234
|
+
html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);
|
|
235
|
+
});
|
|
167
236
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
237
|
+
// Line breaks
|
|
238
|
+
if (lazy_linefeeds) {
|
|
239
|
+
// Lazy linefeeds: single newline becomes <br> (except between paragraphs and after/before block elements)
|
|
240
|
+
const blocks = [];
|
|
241
|
+
let bi = 0;
|
|
242
|
+
|
|
243
|
+
// Protect tables and lists
|
|
244
|
+
html = html.replace(/<(table|[uo]l)[^>]*>[\s\S]*?<\/\1>/g, m => {
|
|
245
|
+
blocks[bi] = m;
|
|
246
|
+
return `§B${bi++}§`;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Handle paragraphs and block elements
|
|
250
|
+
html = html.replace(/\n\n+/g, '§P§')
|
|
251
|
+
// After block elements
|
|
252
|
+
.replace(/(<\/(?:h[1-6]|blockquote|pre)>)\n/g, '$1§N§')
|
|
253
|
+
.replace(/(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)\n/g, '$1§N§')
|
|
254
|
+
// Before block elements
|
|
255
|
+
.replace(/\n(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)/g, '§N§$1')
|
|
256
|
+
.replace(/\n(§B\d+§)/g, '§N§$1')
|
|
257
|
+
.replace(/(§B\d+§)\n/g, '$1§N§')
|
|
258
|
+
// Convert remaining newlines
|
|
259
|
+
.replace(/\n/g, `<br${getAttr('br')}>`)
|
|
260
|
+
// Restore
|
|
261
|
+
.replace(/§N§/g, '\n')
|
|
262
|
+
.replace(/§P§/g, '</p><p>');
|
|
263
|
+
|
|
264
|
+
// Restore protected blocks
|
|
265
|
+
blocks.forEach((b, i) => html = html.replace(`§B${i}§`, b));
|
|
266
|
+
|
|
267
|
+
html = '<p>' + html + '</p>';
|
|
268
|
+
} else {
|
|
269
|
+
// Standard: two spaces at end of line for line breaks
|
|
270
|
+
html = html.replace(/ $/gm, `<br${getAttr('br')}>`);
|
|
271
|
+
|
|
272
|
+
// Paragraphs (double newlines)
|
|
273
|
+
html = html.replace(/\n\n+/g, '</p><p>');
|
|
274
|
+
html = '<p>' + html + '</p>';
|
|
275
|
+
}
|
|
171
276
|
|
|
172
277
|
// Clean up empty paragraphs and unwrap block elements
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
278
|
+
const cleanupPatterns = [
|
|
279
|
+
[/<p><\/p>/g, ''],
|
|
280
|
+
[/<p>(<h[1-6][^>]*>)/g, '$1'],
|
|
281
|
+
[/(<\/h[1-6]>)<\/p>/g, '$1'],
|
|
282
|
+
[/<p>(<blockquote[^>]*>)/g, '$1'],
|
|
283
|
+
[/(<\/blockquote>)<\/p>/g, '$1'],
|
|
284
|
+
[/<p>(<ul[^>]*>|<ol[^>]*>)/g, '$1'],
|
|
285
|
+
[/(<\/ul>|<\/ol>)<\/p>/g, '$1'],
|
|
286
|
+
[/<p>(<hr[^>]*>)<\/p>/g, '$1'],
|
|
287
|
+
[/<p>(<table[^>]*>)/g, '$1'],
|
|
288
|
+
[/(<\/table>)<\/p>/g, '$1'],
|
|
289
|
+
[/<p>(<pre[^>]*>)/g, '$1'],
|
|
290
|
+
[/(<\/pre>)<\/p>/g, '$1'],
|
|
291
|
+
[new RegExp(`<p>(${PLACEHOLDER_CB}\\d+§)<\/p>`, 'g'), '$1']
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
cleanupPatterns.forEach(([pattern, replacement]) => {
|
|
295
|
+
html = html.replace(pattern, replacement);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Phase 4: Restore code blocks and inline code
|
|
183
299
|
|
|
184
300
|
// Restore code blocks
|
|
185
301
|
codeBlocks.forEach((block, i) => {
|
|
186
|
-
const placeholder = `§CB${i}§`;
|
|
187
302
|
let replacement;
|
|
188
303
|
|
|
189
|
-
if (
|
|
304
|
+
if (block.custom && fence_plugin) {
|
|
305
|
+
// Use custom fence plugin
|
|
190
306
|
replacement = fence_plugin(block.code, block.lang);
|
|
307
|
+
// If plugin returns undefined, fall back to default rendering
|
|
191
308
|
if (replacement === undefined) {
|
|
192
|
-
|
|
309
|
+
const langClass = !inline_styles && block.lang ? ` class="language-${block.lang}"` : '';
|
|
310
|
+
const codeAttr = inline_styles ? getAttr('code') : langClass;
|
|
311
|
+
const langAttr = bidirectional && block.lang ? ` data-qd-lang="${escapeHtml(block.lang)}"` : '';
|
|
312
|
+
const fenceAttr = bidirectional ? ` data-qd-fence="${escapeHtml(block.fence)}"` : '';
|
|
313
|
+
replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${escapeHtml(block.code)}</code></pre>`;
|
|
193
314
|
}
|
|
194
315
|
} else {
|
|
195
|
-
|
|
316
|
+
// Default rendering
|
|
317
|
+
const langClass = !inline_styles && block.lang ? ` class="language-${block.lang}"` : '';
|
|
318
|
+
const codeAttr = inline_styles ? getAttr('code') : langClass;
|
|
319
|
+
const langAttr = bidirectional && block.lang ? ` data-qd-lang="${escapeHtml(block.lang)}"` : '';
|
|
320
|
+
const fenceAttr = bidirectional ? ` data-qd-fence="${escapeHtml(block.fence)}"` : '';
|
|
321
|
+
replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${block.code}</code></pre>`;
|
|
196
322
|
}
|
|
197
323
|
|
|
324
|
+
const placeholder = `${PLACEHOLDER_CB}${i}§`;
|
|
198
325
|
html = html.replace(placeholder, replacement);
|
|
199
326
|
});
|
|
200
327
|
|
|
201
|
-
// Restore inline
|
|
202
|
-
inlineCodes.forEach((
|
|
203
|
-
const placeholder =
|
|
204
|
-
html = html.replace(placeholder, `<code${getAttr('code'
|
|
328
|
+
// Restore inline code
|
|
329
|
+
inlineCodes.forEach((code, i) => {
|
|
330
|
+
const placeholder = `${PLACEHOLDER_IC}${i}§`;
|
|
331
|
+
html = html.replace(placeholder, `<code${getAttr('code')}${dataQd('`')}>${code}</code>`);
|
|
205
332
|
});
|
|
206
333
|
|
|
207
334
|
return html.trim();
|
|
208
335
|
}
|
|
209
336
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
let listItemContent = content;
|
|
229
|
-
let taskAttrs = '';
|
|
230
|
-
const taskMatch = content.match(/^\[([x ])\]\s+(.*)$/i);
|
|
231
|
-
if (taskMatch && !isOrdered) {
|
|
232
|
-
const [, checked, taskContent] = taskMatch;
|
|
233
|
-
const isChecked = checked.toLowerCase() === 'x';
|
|
234
|
-
listItemContent = `<input type="checkbox"${getAttr('task-checkbox', '', '[')}${isChecked ? ' checked' : ''}> ${taskContent}`;
|
|
235
|
-
taskAttrs = getAttr('task-item', '', '- [ ]');
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Close deeper levels
|
|
239
|
-
while (listStack.length > level + 1) {
|
|
240
|
-
const list = listStack.pop();
|
|
241
|
-
result.push(`</${list.type}>`);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Open new level if needed
|
|
245
|
-
if (listStack.length === level) {
|
|
246
|
-
listStack.push({ type: listType, level, marker: sourceMarker });
|
|
247
|
-
result.push(`<${listType}${getAttr(listType, '', sourceMarker)}>`);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const liAttr = taskAttrs || getAttr('li', '', sourceMarker);
|
|
251
|
-
result.push(`<li${liAttr}>${listItemContent}</li>`);
|
|
252
|
-
} else {
|
|
253
|
-
// Close all lists
|
|
254
|
-
while (listStack.length > 0) {
|
|
255
|
-
const list = listStack.pop();
|
|
256
|
-
result.push(`</${list.type}>`);
|
|
257
|
-
}
|
|
258
|
-
result.push(line);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Close remaining lists
|
|
263
|
-
while (listStack.length > 0) {
|
|
264
|
-
const list = listStack.pop();
|
|
265
|
-
result.push(`</${list.type}>`);
|
|
266
|
-
}
|
|
337
|
+
/**
|
|
338
|
+
* Process inline markdown formatting
|
|
339
|
+
*/
|
|
340
|
+
function processInlineMarkdown(text, getAttr) {
|
|
341
|
+
|
|
342
|
+
// Process inline formatting patterns
|
|
343
|
+
const patterns = [
|
|
344
|
+
[/\*\*(.+?)\*\*/g, 'strong'],
|
|
345
|
+
[/__(.+?)__/g, 'strong'],
|
|
346
|
+
[/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, 'em'],
|
|
347
|
+
[/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em'],
|
|
348
|
+
[/~~(.+?)~~/g, 'del'],
|
|
349
|
+
[/`([^`]+)`/g, 'code']
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
patterns.forEach(([pattern, tag]) => {
|
|
353
|
+
text = text.replace(pattern, `<${tag}${getAttr(tag)}>$1</${tag}>`);
|
|
354
|
+
});
|
|
267
355
|
|
|
268
|
-
return
|
|
356
|
+
return text;
|
|
269
357
|
}
|
|
270
358
|
|
|
271
|
-
|
|
272
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Process markdown tables
|
|
361
|
+
*/
|
|
362
|
+
function processTable(text, getAttr) {
|
|
273
363
|
const lines = text.split('\n');
|
|
274
364
|
const result = [];
|
|
275
365
|
let inTable = false;
|
|
@@ -278,18 +368,22 @@ function processTablesBD(text, getAttr) {
|
|
|
278
368
|
for (let i = 0; i < lines.length; i++) {
|
|
279
369
|
const line = lines[i].trim();
|
|
280
370
|
|
|
281
|
-
if
|
|
371
|
+
// Check if this line looks like a table row (with or without trailing |)
|
|
372
|
+
if (line.includes('|') && (line.startsWith('|') || /[^\\|]/.test(line))) {
|
|
282
373
|
if (!inTable) {
|
|
283
374
|
inTable = true;
|
|
284
375
|
tableLines = [];
|
|
285
376
|
}
|
|
286
377
|
tableLines.push(line);
|
|
287
378
|
} else {
|
|
379
|
+
// Not a table line
|
|
288
380
|
if (inTable) {
|
|
289
|
-
|
|
381
|
+
// Process the accumulated table
|
|
382
|
+
const tableHtml = buildTable(tableLines, getAttr);
|
|
290
383
|
if (tableHtml) {
|
|
291
384
|
result.push(tableHtml);
|
|
292
385
|
} else {
|
|
386
|
+
// Not a valid table, restore original lines
|
|
293
387
|
result.push(...tableLines);
|
|
294
388
|
}
|
|
295
389
|
inTable = false;
|
|
@@ -299,8 +393,9 @@ function processTablesBD(text, getAttr) {
|
|
|
299
393
|
}
|
|
300
394
|
}
|
|
301
395
|
|
|
396
|
+
// Handle table at end of text
|
|
302
397
|
if (inTable && tableLines.length > 0) {
|
|
303
|
-
const tableHtml =
|
|
398
|
+
const tableHtml = buildTable(tableLines, getAttr);
|
|
304
399
|
if (tableHtml) {
|
|
305
400
|
result.push(tableHtml);
|
|
306
401
|
} else {
|
|
@@ -311,53 +406,68 @@ function processTablesBD(text, getAttr) {
|
|
|
311
406
|
return result.join('\n');
|
|
312
407
|
}
|
|
313
408
|
|
|
314
|
-
|
|
315
|
-
|
|
409
|
+
/**
|
|
410
|
+
* Build an HTML table from markdown table lines
|
|
411
|
+
*/
|
|
412
|
+
function buildTable(lines, getAttr) {
|
|
413
|
+
|
|
316
414
|
if (lines.length < 2) return null;
|
|
317
415
|
|
|
318
|
-
//
|
|
416
|
+
// Check for separator line (second line should be the separator)
|
|
319
417
|
let separatorIndex = -1;
|
|
320
|
-
let alignments = [];
|
|
321
|
-
|
|
322
418
|
for (let i = 1; i < lines.length; i++) {
|
|
419
|
+
// Support separator with or without leading/trailing pipes
|
|
323
420
|
if (/^\|?[\s\-:|]+\|?$/.test(lines[i]) && lines[i].includes('-')) {
|
|
324
421
|
separatorIndex = i;
|
|
325
|
-
const cells = lines[i].replace(/^\|/, '').replace(/\|$/, '').split('|');
|
|
326
|
-
alignments = cells.map(cell => {
|
|
327
|
-
const trimmed = cell.trim();
|
|
328
|
-
if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';
|
|
329
|
-
if (trimmed.endsWith(':')) return 'right';
|
|
330
|
-
return 'left';
|
|
331
|
-
});
|
|
332
422
|
break;
|
|
333
423
|
}
|
|
334
424
|
}
|
|
335
425
|
|
|
336
426
|
if (separatorIndex === -1) return null;
|
|
337
427
|
|
|
338
|
-
|
|
428
|
+
const headerLines = lines.slice(0, separatorIndex);
|
|
429
|
+
const bodyLines = lines.slice(separatorIndex + 1);
|
|
339
430
|
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
431
|
+
// Parse alignment from separator
|
|
432
|
+
const separator = lines[separatorIndex];
|
|
433
|
+
// Handle pipes at start/end or not
|
|
434
|
+
const separatorCells = separator.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
|
|
435
|
+
const alignments = separatorCells.map(cell => {
|
|
436
|
+
const trimmed = cell.trim();
|
|
437
|
+
if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';
|
|
438
|
+
if (trimmed.endsWith(':')) return 'right';
|
|
439
|
+
return 'left';
|
|
440
|
+
});
|
|
350
441
|
|
|
351
|
-
|
|
352
|
-
|
|
442
|
+
let html = `<table${getAttr('table')}>\n`;
|
|
443
|
+
|
|
444
|
+
// Build header
|
|
445
|
+
// Note: headerLines will always have length > 0 since separatorIndex starts from 1
|
|
446
|
+
html += `<thead${getAttr('thead')}>\n`;
|
|
447
|
+
headerLines.forEach(line => {
|
|
448
|
+
html += `<tr${getAttr('tr')}>\n`;
|
|
449
|
+
// Handle pipes at start/end or not
|
|
450
|
+
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
|
|
451
|
+
cells.forEach((cell, i) => {
|
|
452
|
+
const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
|
|
453
|
+
const processedCell = processInlineMarkdown(cell.trim(), getAttr);
|
|
454
|
+
html += `<th${getAttr('th', alignStyle)}>${processedCell}</th>\n`;
|
|
455
|
+
});
|
|
456
|
+
html += '</tr>\n';
|
|
457
|
+
});
|
|
458
|
+
html += '</thead>\n';
|
|
459
|
+
|
|
460
|
+
// Build body
|
|
353
461
|
if (bodyLines.length > 0) {
|
|
354
|
-
html += `<tbody${getAttr('tbody'
|
|
462
|
+
html += `<tbody${getAttr('tbody')}>\n`;
|
|
355
463
|
bodyLines.forEach(line => {
|
|
356
|
-
html += `<tr${getAttr('tr'
|
|
357
|
-
|
|
464
|
+
html += `<tr${getAttr('tr')}>\n`;
|
|
465
|
+
// Handle pipes at start/end or not
|
|
466
|
+
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
|
|
358
467
|
cells.forEach((cell, i) => {
|
|
359
|
-
const
|
|
360
|
-
|
|
468
|
+
const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
|
|
469
|
+
const processedCell = processInlineMarkdown(cell.trim(), getAttr);
|
|
470
|
+
html += `<td${getAttr('td', alignStyle)}>${processedCell}</td>\n`;
|
|
361
471
|
});
|
|
362
472
|
html += '</tr>\n';
|
|
363
473
|
});
|
|
@@ -369,10 +479,193 @@ function buildTableBD(lines, getAttr) {
|
|
|
369
479
|
}
|
|
370
480
|
|
|
371
481
|
/**
|
|
372
|
-
*
|
|
373
|
-
|
|
374
|
-
|
|
482
|
+
* Process markdown lists (ordered and unordered)
|
|
483
|
+
*/
|
|
484
|
+
function processLists(text, getAttr, inline_styles, bidirectional) {
|
|
485
|
+
|
|
486
|
+
const lines = text.split('\n');
|
|
487
|
+
const result = [];
|
|
488
|
+
let listStack = []; // Track nested lists
|
|
489
|
+
|
|
490
|
+
// Helper to escape HTML for data-qd attributes
|
|
491
|
+
const escapeHtml = (text) => text.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]);
|
|
492
|
+
const dataQd = bidirectional ? (marker) => ` data-qd="${escapeHtml(marker)}"` : () => '';
|
|
493
|
+
|
|
494
|
+
for (let i = 0; i < lines.length; i++) {
|
|
495
|
+
const line = lines[i];
|
|
496
|
+
const match = line.match(/^(\s*)([*\-+]|\d+\.)\s+(.+)$/);
|
|
497
|
+
|
|
498
|
+
if (match) {
|
|
499
|
+
const [, indent, marker, content] = match;
|
|
500
|
+
const level = Math.floor(indent.length / 2);
|
|
501
|
+
const isOrdered = /^\d+\./.test(marker);
|
|
502
|
+
const listType = isOrdered ? 'ol' : 'ul';
|
|
503
|
+
|
|
504
|
+
// Check for task list items
|
|
505
|
+
let listItemContent = content;
|
|
506
|
+
let taskListClass = '';
|
|
507
|
+
const taskMatch = content.match(/^\[([x ])\]\s+(.*)$/i);
|
|
508
|
+
if (taskMatch && !isOrdered) {
|
|
509
|
+
const [, checked, taskContent] = taskMatch;
|
|
510
|
+
const isChecked = checked.toLowerCase() === 'x';
|
|
511
|
+
const checkboxAttr = inline_styles
|
|
512
|
+
? ' style="margin-right:.5em"'
|
|
513
|
+
: ` class="${CLASS_PREFIX}task-checkbox"`;
|
|
514
|
+
listItemContent = `<input type="checkbox"${checkboxAttr}${isChecked ? ' checked' : ''} disabled> ${taskContent}`;
|
|
515
|
+
taskListClass = inline_styles ? ' style="list-style:none"' : ` class="${CLASS_PREFIX}task-item"`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Close deeper levels
|
|
519
|
+
while (listStack.length > level + 1) {
|
|
520
|
+
const list = listStack.pop();
|
|
521
|
+
result.push(`</${list.type}>`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Open new level if needed
|
|
525
|
+
if (listStack.length === level) {
|
|
526
|
+
// Need to open a new list
|
|
527
|
+
listStack.push({ type: listType, level });
|
|
528
|
+
result.push(`<${listType}${getAttr(listType)}>`);
|
|
529
|
+
} else if (listStack.length === level + 1) {
|
|
530
|
+
// Check if we need to switch list type
|
|
531
|
+
const currentList = listStack[listStack.length - 1];
|
|
532
|
+
if (currentList.type !== listType) {
|
|
533
|
+
result.push(`</${currentList.type}>`);
|
|
534
|
+
listStack.pop();
|
|
535
|
+
listStack.push({ type: listType, level });
|
|
536
|
+
result.push(`<${listType}${getAttr(listType)}>`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const liAttr = taskListClass || getAttr('li');
|
|
541
|
+
result.push(`<li${liAttr}${dataQd(marker)}>${listItemContent}</li>`);
|
|
542
|
+
} else {
|
|
543
|
+
// Not a list item, close all lists
|
|
544
|
+
while (listStack.length > 0) {
|
|
545
|
+
const list = listStack.pop();
|
|
546
|
+
result.push(`</${list.type}>`);
|
|
547
|
+
}
|
|
548
|
+
result.push(line);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Close any remaining lists
|
|
553
|
+
while (listStack.length > 0) {
|
|
554
|
+
const list = listStack.pop();
|
|
555
|
+
result.push(`</${list.type}>`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return result.join('\n');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Emit CSS styles for quikdown elements
|
|
563
|
+
* @param {string} prefix - Optional class prefix (default: 'quikdown-')
|
|
564
|
+
* @param {string} theme - Optional theme: 'light' (default) or 'dark'
|
|
565
|
+
* @returns {string} CSS string with quikdown styles
|
|
566
|
+
*/
|
|
567
|
+
quikdown.emitStyles = function(prefix = 'quikdown-', theme = 'light') {
|
|
568
|
+
const styles = QUIKDOWN_STYLES;
|
|
569
|
+
|
|
570
|
+
// Define theme color overrides
|
|
571
|
+
const themeOverrides = {
|
|
572
|
+
dark: {
|
|
573
|
+
'#f4f4f4': '#2a2a2a', // pre background
|
|
574
|
+
'#f0f0f0': '#2a2a2a', // code background
|
|
575
|
+
'#f2f2f2': '#2a2a2a', // th background
|
|
576
|
+
'#ddd': '#3a3a3a', // borders
|
|
577
|
+
'#06c': '#6db3f2', // links
|
|
578
|
+
_textColor: '#e0e0e0'
|
|
579
|
+
},
|
|
580
|
+
light: {
|
|
581
|
+
_textColor: '#333' // Explicit text color for light theme
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
let css = '';
|
|
586
|
+
for (const [tag, style] of Object.entries(styles)) {
|
|
587
|
+
let themedStyle = style;
|
|
588
|
+
|
|
589
|
+
// Apply theme overrides if dark theme
|
|
590
|
+
if (theme === 'dark' && themeOverrides.dark) {
|
|
591
|
+
// Replace colors
|
|
592
|
+
for (const [oldColor, newColor] of Object.entries(themeOverrides.dark)) {
|
|
593
|
+
if (!oldColor.startsWith('_')) {
|
|
594
|
+
themedStyle = themedStyle.replace(new RegExp(oldColor, 'g'), newColor);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Add text color for certain elements in dark theme
|
|
599
|
+
const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
|
|
600
|
+
if (needsTextColor.includes(tag)) {
|
|
601
|
+
themedStyle += `;color:${themeOverrides.dark._textColor}`;
|
|
602
|
+
}
|
|
603
|
+
} else if (theme === 'light' && themeOverrides.light) {
|
|
604
|
+
// Add explicit text color for light theme elements too
|
|
605
|
+
const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
|
|
606
|
+
if (needsTextColor.includes(tag)) {
|
|
607
|
+
themedStyle += `;color:${themeOverrides.light._textColor}`;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
css += `.${prefix}${tag} { ${themedStyle} }\n`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return css;
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Configure quikdown with options and return a function
|
|
619
|
+
* @param {Object} options - Configuration options
|
|
620
|
+
* @returns {Function} Configured quikdown function
|
|
621
|
+
*/
|
|
622
|
+
quikdown.configure = function(options) {
|
|
623
|
+
return function(markdown) {
|
|
624
|
+
return quikdown(markdown, options);
|
|
625
|
+
};
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Version information
|
|
630
|
+
*/
|
|
631
|
+
quikdown.version = quikdownVersion;
|
|
632
|
+
|
|
633
|
+
// Export for both CommonJS and ES6
|
|
634
|
+
/* istanbul ignore next */
|
|
635
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
636
|
+
module.exports = quikdown;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// For browser global
|
|
640
|
+
/* istanbul ignore next */
|
|
641
|
+
if (typeof window !== 'undefined') {
|
|
642
|
+
window.quikdown = quikdown;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* quikdown_bd - Bidirectional markdown/HTML converter
|
|
647
|
+
* Extends core quikdown with HTML→Markdown conversion
|
|
648
|
+
*
|
|
649
|
+
* Uses data-qd attributes to preserve original markdown syntax
|
|
650
|
+
* Enables HTML→Markdown conversion for quikdown-generated HTML
|
|
651
|
+
*/
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Create bidirectional version by extending quikdown
|
|
656
|
+
* This wraps quikdown and adds the toMarkdown method
|
|
375
657
|
*/
|
|
658
|
+
function quikdown_bd(markdown, options = {}) {
|
|
659
|
+
// Use core quikdown with bidirectional flag to add data-qd attributes
|
|
660
|
+
return quikdown(markdown, { ...options, bidirectional: true });
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Copy all properties and methods from quikdown (including version)
|
|
664
|
+
Object.keys(quikdown).forEach(key => {
|
|
665
|
+
quikdown_bd[key] = quikdown[key];
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Add the toMarkdown method for HTML→Markdown conversion
|
|
376
669
|
quikdown_bd.toMarkdown = function(htmlOrElement) {
|
|
377
670
|
// Accept either HTML string or DOM element
|
|
378
671
|
let container;
|
|
@@ -380,6 +673,7 @@ quikdown_bd.toMarkdown = function(htmlOrElement) {
|
|
|
380
673
|
container = document.createElement('div');
|
|
381
674
|
container.innerHTML = htmlOrElement;
|
|
382
675
|
} else if (htmlOrElement instanceof Element) {
|
|
676
|
+
/* istanbul ignore next - browser-only code path, not testable in jsdom */
|
|
383
677
|
container = htmlOrElement;
|
|
384
678
|
} else {
|
|
385
679
|
return '';
|
|
@@ -398,7 +692,6 @@ quikdown_bd.toMarkdown = function(htmlOrElement) {
|
|
|
398
692
|
|
|
399
693
|
const tag = node.tagName.toLowerCase();
|
|
400
694
|
const dataQd = node.getAttribute('data-qd');
|
|
401
|
-
const styles = window.getComputedStyle ? window.getComputedStyle(node) : {};
|
|
402
695
|
|
|
403
696
|
// Process children with context
|
|
404
697
|
let childContent = '';
|
|
@@ -420,33 +713,26 @@ quikdown_bd.toMarkdown = function(htmlOrElement) {
|
|
|
420
713
|
|
|
421
714
|
case 'strong':
|
|
422
715
|
case 'b':
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
return `${boldMarker}${childContent}${boldMarker}`;
|
|
427
|
-
}
|
|
428
|
-
return childContent;
|
|
716
|
+
if (!childContent) return ''; // Don't add markers for empty content
|
|
717
|
+
const boldMarker = dataQd || '**';
|
|
718
|
+
return `${boldMarker}${childContent}${boldMarker}`;
|
|
429
719
|
|
|
430
720
|
case 'em':
|
|
431
721
|
case 'i':
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
return `${emMarker}${childContent}${emMarker}`;
|
|
436
|
-
}
|
|
437
|
-
return childContent;
|
|
722
|
+
if (!childContent) return ''; // Don't add markers for empty content
|
|
723
|
+
const emMarker = dataQd || '*';
|
|
724
|
+
return `${emMarker}${childContent}${emMarker}`;
|
|
438
725
|
|
|
439
726
|
case 'del':
|
|
440
727
|
case 's':
|
|
441
728
|
case 'strike':
|
|
729
|
+
if (!childContent) return ''; // Don't add markers for empty content
|
|
442
730
|
const delMarker = dataQd || '~~';
|
|
443
731
|
return `${delMarker}${childContent}${delMarker}`;
|
|
444
732
|
|
|
445
733
|
case 'code':
|
|
446
|
-
//
|
|
447
|
-
if (
|
|
448
|
-
return childContent;
|
|
449
|
-
}
|
|
734
|
+
// Note: code inside pre is handled directly by the pre case using querySelector
|
|
735
|
+
if (!childContent) return ''; // Don't add markers for empty content
|
|
450
736
|
const codeMarker = dataQd || '`';
|
|
451
737
|
return `${codeMarker}${childContent}${codeMarker}`;
|
|
452
738
|
|
|
@@ -509,7 +795,30 @@ quikdown_bd.toMarkdown = function(htmlOrElement) {
|
|
|
509
795
|
if (node.classList && node.classList.contains('mermaid-container')) {
|
|
510
796
|
const fence = node.getAttribute('data-qd-fence') || '```';
|
|
511
797
|
const lang = node.getAttribute('data-qd-lang') || 'mermaid';
|
|
512
|
-
|
|
798
|
+
|
|
799
|
+
// First check for data-qd-source attribute on the container
|
|
800
|
+
const source = node.getAttribute('data-qd-source');
|
|
801
|
+
if (source) {
|
|
802
|
+
// Decode HTML entities from the attribute (mainly ")
|
|
803
|
+
const temp = document.createElement('textarea');
|
|
804
|
+
temp.innerHTML = source;
|
|
805
|
+
const code = temp.value;
|
|
806
|
+
return `${fence}${lang}\n${code}\n${fence}\n\n`;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Check for source on the pre.mermaid element
|
|
810
|
+
const mermaidPre = node.querySelector('pre.mermaid');
|
|
811
|
+
if (mermaidPre) {
|
|
812
|
+
const preSource = mermaidPre.getAttribute('data-qd-source');
|
|
813
|
+
if (preSource) {
|
|
814
|
+
const temp = document.createElement('textarea');
|
|
815
|
+
temp.innerHTML = preSource;
|
|
816
|
+
const code = temp.value;
|
|
817
|
+
return `${fence}${lang}\n${code}\n${fence}\n\n`;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Fallback: Look for the legacy .mermaid-source element
|
|
513
822
|
const sourceElement = node.querySelector('.mermaid-source');
|
|
514
823
|
if (sourceElement) {
|
|
515
824
|
// Decode HTML entities
|
|
@@ -518,7 +827,8 @@ quikdown_bd.toMarkdown = function(htmlOrElement) {
|
|
|
518
827
|
const code = temp.textContent;
|
|
519
828
|
return `${fence}${lang}\n${code}\n${fence}\n\n`;
|
|
520
829
|
}
|
|
521
|
-
|
|
830
|
+
|
|
831
|
+
// Final fallback: try to extract from the mermaid element (unreliable after rendering)
|
|
522
832
|
const mermaidElement = node.querySelector('.mermaid');
|
|
523
833
|
if (mermaidElement && mermaidElement.textContent.includes('graph')) {
|
|
524
834
|
return `${fence}${lang}\n${mermaidElement.textContent.trim()}\n${fence}\n\n`;
|
|
@@ -609,7 +919,7 @@ quikdown_bd.toMarkdown = function(htmlOrElement) {
|
|
|
609
919
|
|
|
610
920
|
// Add separator with alignment
|
|
611
921
|
const separators = headers.map((_, i) => {
|
|
612
|
-
const align = alignments[i] ||
|
|
922
|
+
const align = alignments[i] || 'left';
|
|
613
923
|
if (align === 'center') return ':---:';
|
|
614
924
|
if (align === 'right') return '---:';
|
|
615
925
|
return '---';
|
|
@@ -645,29 +955,23 @@ quikdown_bd.toMarkdown = function(htmlOrElement) {
|
|
|
645
955
|
return markdown;
|
|
646
956
|
};
|
|
647
957
|
|
|
648
|
-
//
|
|
649
|
-
quikdown_bd.emitStyles = function(prefix = 'quikdown-', theme = 'light') {
|
|
650
|
-
// This would generate CSS based on the styles
|
|
651
|
-
// For now, returning empty string as placeholder
|
|
652
|
-
// In production, this would generate the full CSS
|
|
653
|
-
return '';
|
|
654
|
-
};
|
|
655
|
-
|
|
656
|
-
// Configure method
|
|
958
|
+
// Override the configure method to return a bidirectional version
|
|
657
959
|
quikdown_bd.configure = function(options) {
|
|
658
960
|
return function(markdown) {
|
|
659
961
|
return quikdown_bd(markdown, options);
|
|
660
962
|
};
|
|
661
963
|
};
|
|
662
964
|
|
|
663
|
-
//
|
|
664
|
-
|
|
965
|
+
// Set version
|
|
966
|
+
// Version is already copied from quikdown via Object.keys loop
|
|
665
967
|
|
|
666
968
|
// Export for both module and browser
|
|
969
|
+
/* istanbul ignore next */
|
|
667
970
|
if (typeof module !== 'undefined' && module.exports) {
|
|
668
971
|
module.exports = quikdown_bd;
|
|
669
972
|
}
|
|
670
973
|
|
|
974
|
+
/* istanbul ignore next */
|
|
671
975
|
if (typeof window !== 'undefined') {
|
|
672
976
|
window.quikdown_bd = quikdown_bd;
|
|
673
977
|
}
|