quikdown 1.2.9 → 1.2.11
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 +5 -5
- package/dist/quikdown.cjs +104 -5
- package/dist/quikdown.d.ts +12 -0
- package/dist/quikdown.dark.css +1 -1
- package/dist/quikdown.esm.js +104 -5
- 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 +104 -5
- 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 +16 -28
- package/dist/quikdown_ast.esm.js +16 -28
- package/dist/quikdown_ast.esm.min.js +2 -2
- package/dist/quikdown_ast.esm.min.js.gz +0 -0
- package/dist/quikdown_ast.esm.min.js.map +1 -1
- package/dist/quikdown_ast.umd.js +16 -28
- package/dist/quikdown_ast.umd.min.js +2 -2
- package/dist/quikdown_ast.umd.min.js.gz +0 -0
- package/dist/quikdown_ast.umd.min.js.map +1 -1
- package/dist/quikdown_ast_html.cjs +17 -29
- package/dist/quikdown_ast_html.esm.js +17 -29
- 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.esm.min.js.map +1 -1
- package/dist/quikdown_ast_html.umd.js +17 -29
- package/dist/quikdown_ast_html.umd.min.js +2 -2
- package/dist/quikdown_ast_html.umd.min.js.gz +0 -0
- package/dist/quikdown_ast_html.umd.min.js.map +1 -1
- package/dist/quikdown_bd.cjs +104 -5
- package/dist/quikdown_bd.esm.js +104 -5
- 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 +104 -5
- 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 +244 -12
- package/dist/quikdown_edit.esm.js +244 -12
- 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 +244 -12
- 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 +17 -29
- package/dist/quikdown_json.esm.js +17 -29
- package/dist/quikdown_json.esm.min.js +2 -2
- package/dist/quikdown_json.esm.min.js.gz +0 -0
- package/dist/quikdown_json.esm.min.js.map +1 -1
- package/dist/quikdown_json.umd.js +17 -29
- package/dist/quikdown_json.umd.min.js +2 -2
- package/dist/quikdown_json.umd.min.js.gz +0 -0
- package/dist/quikdown_json.umd.min.js.map +1 -1
- package/dist/quikdown_yaml.cjs +17 -29
- package/dist/quikdown_yaml.esm.js +17 -29
- package/dist/quikdown_yaml.esm.min.js +2 -2
- package/dist/quikdown_yaml.esm.min.js.gz +0 -0
- package/dist/quikdown_yaml.esm.min.js.map +1 -1
- package/dist/quikdown_yaml.umd.js +17 -29
- package/dist/quikdown_yaml.umd.min.js +2 -2
- package/dist/quikdown_yaml.umd.min.js.gz +0 -0
- package/dist/quikdown_yaml.umd.min.js.map +1 -1
- package/package.json +10 -10
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.11
|
|
4
4
|
* @license BSD-2-Clause
|
|
5
5
|
* @copyright DeftIO 2025
|
|
6
6
|
*/
|
|
@@ -224,7 +224,7 @@ function looksLikeTableRow(line) {
|
|
|
224
224
|
// ────────────────────────────────────────────────────────────────────
|
|
225
225
|
|
|
226
226
|
/** Build-time version stamp (injected by tools/updateVersion) */
|
|
227
|
-
const quikdownVersion = '1.2.
|
|
227
|
+
const quikdownVersion = '1.2.11';
|
|
228
228
|
|
|
229
229
|
/** CSS class prefix used for all generated elements */
|
|
230
230
|
const CLASS_PREFIX = 'quikdown-';
|
|
@@ -232,6 +232,10 @@ const CLASS_PREFIX = 'quikdown-';
|
|
|
232
232
|
/** Placeholder sigils — chosen to be extremely unlikely in real text */
|
|
233
233
|
const PLACEHOLDER_CB = '§CB'; // fenced code blocks
|
|
234
234
|
const PLACEHOLDER_IC = '§IC'; // inline code spans
|
|
235
|
+
const PLACEHOLDER_HT = '§HT'; // safe HTML tags (limited mode)
|
|
236
|
+
|
|
237
|
+
/** Attributes whose values need URL sanitization */
|
|
238
|
+
const URL_ATTRIBUTES = { href:1, src:1, action:1, formaction:1 };
|
|
235
239
|
|
|
236
240
|
/** HTML entity escape map */
|
|
237
241
|
const ESC_MAP = {'&':'&','<':'<','>':'>','"':'"',"'":'''};
|
|
@@ -366,6 +370,46 @@ function quikdown(markdown, options = {}) {
|
|
|
366
370
|
return trimmedUrl;
|
|
367
371
|
}
|
|
368
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Sanitize attributes on an HTML tag string for limited mode.
|
|
375
|
+
* Strips on* event handlers (case-insensitive) and runs sanitizeUrl()
|
|
376
|
+
* on href/src/action/formaction values.
|
|
377
|
+
*/
|
|
378
|
+
function sanitizeHtmlTagAttrs(tagStr) {
|
|
379
|
+
// Self-closing or void tag without attributes — pass through
|
|
380
|
+
if (!/\s/.test(tagStr.replace(/<\/?[a-zA-Z][a-zA-Z0-9]*/, '').replace(/\/?>$/, ''))) {
|
|
381
|
+
return tagStr;
|
|
382
|
+
}
|
|
383
|
+
// Parse: <tagname ...attrs... > or <tagname ...attrs... />
|
|
384
|
+
const m = tagStr.match(/^(<\/?[a-zA-Z][a-zA-Z0-9]*)([\s\S]*?)(\/?>)$/);
|
|
385
|
+
/* istanbul ignore next - defensive: Phase 1.5 regex guarantees valid tag shape */
|
|
386
|
+
if (!m) return tagStr;
|
|
387
|
+
|
|
388
|
+
const [, open, attrStr, close] = m;
|
|
389
|
+
// Match individual attributes: name="value", name='value', name=value, or bare name
|
|
390
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- linear: no nested quantifiers
|
|
391
|
+
const attrRe = /([a-zA-Z_][\w\-.:]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
392
|
+
const attrs = [];
|
|
393
|
+
let am;
|
|
394
|
+
while ((am = attrRe.exec(attrStr)) !== null) {
|
|
395
|
+
const name = am[1];
|
|
396
|
+
const value = am[2] !== undefined ? am[2] : am[3] !== undefined ? am[3] : am[4];
|
|
397
|
+
// Strip event handlers (on*)
|
|
398
|
+
if (/^on/i.test(name)) continue;
|
|
399
|
+
if (value === undefined) {
|
|
400
|
+
// Boolean attribute (e.g. disabled, checked)
|
|
401
|
+
attrs.push(name);
|
|
402
|
+
} else {
|
|
403
|
+
let sanitized = value;
|
|
404
|
+
if (name.toLowerCase() in URL_ATTRIBUTES) {
|
|
405
|
+
sanitized = sanitizeUrl(value);
|
|
406
|
+
}
|
|
407
|
+
attrs.push(`${name}="${sanitized}"`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return open + (attrs.length ? ' ' + attrs.join(' ') : '') + close;
|
|
411
|
+
}
|
|
412
|
+
|
|
369
413
|
// ────────────────────────────────────────────────────────────────
|
|
370
414
|
// Phase 1 — Code Extraction
|
|
371
415
|
// ────────────────────────────────────────────────────────────────
|
|
@@ -417,17 +461,57 @@ function quikdown(markdown, options = {}) {
|
|
|
417
461
|
return placeholder;
|
|
418
462
|
});
|
|
419
463
|
|
|
464
|
+
// ────────────────────────────────────────────────────────────────
|
|
465
|
+
// Phase 1.5 — Safe HTML Extraction (whitelist mode)
|
|
466
|
+
// ────────────────────────────────────────────────────────────────
|
|
467
|
+
// When allow_unsafe_html is an object or array, extract whitelisted
|
|
468
|
+
// HTML tags, sanitize their attributes, and replace with placeholders.
|
|
469
|
+
// Non-whitelisted tags stay in text so Phase 2 will escape them.
|
|
470
|
+
|
|
471
|
+
const safeTags = [];
|
|
472
|
+
// Normalize: array → object for O(1) lookup; object used as-is
|
|
473
|
+
const htmlAllow = Array.isArray(allow_unsafe_html)
|
|
474
|
+
? Object.fromEntries(allow_unsafe_html.map(t => [t, 1]))
|
|
475
|
+
: (allow_unsafe_html && typeof allow_unsafe_html === 'object') ? allow_unsafe_html : null;
|
|
476
|
+
|
|
477
|
+
if (htmlAllow) {
|
|
478
|
+
// Pass through HTML comments — browsers render them as nothing
|
|
479
|
+
html = html.replace(/<!--[\s\S]*?-->/g, (match) => {
|
|
480
|
+
const idx = safeTags.length;
|
|
481
|
+
safeTags.push(match);
|
|
482
|
+
return `${PLACEHOLDER_HT}${idx}§`;
|
|
483
|
+
});
|
|
484
|
+
html = html.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/g, (match, tagName) => {
|
|
485
|
+
if (tagName.toLowerCase() in htmlAllow) {
|
|
486
|
+
const sanitized = sanitizeHtmlTagAttrs(match);
|
|
487
|
+
const idx = safeTags.length;
|
|
488
|
+
safeTags.push(sanitized);
|
|
489
|
+
return `${PLACEHOLDER_HT}${idx}§`;
|
|
490
|
+
}
|
|
491
|
+
// Not whitelisted — leave in text for Phase 2 to escape
|
|
492
|
+
return match;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
420
496
|
// ────────────────────────────────────────────────────────────────
|
|
421
497
|
// Phase 2 — HTML Escaping
|
|
422
498
|
// ────────────────────────────────────────────────────────────────
|
|
423
499
|
// All remaining text (everything except code placeholders) is escaped
|
|
424
500
|
// to prevent XSS. The `allow_unsafe_html` option skips this for
|
|
425
501
|
// trusted pipelines that intentionally embed raw HTML.
|
|
502
|
+
// For whitelist mode, escaping still runs (only `true` bypasses it).
|
|
426
503
|
|
|
427
|
-
if (
|
|
504
|
+
if (allow_unsafe_html !== true) {
|
|
428
505
|
html = escapeHtml(html);
|
|
429
506
|
}
|
|
430
507
|
|
|
508
|
+
// Restore safe HTML tag placeholders after escaping
|
|
509
|
+
if (htmlAllow) {
|
|
510
|
+
safeTags.forEach((tag, i) => {
|
|
511
|
+
html = html.replace(`${PLACEHOLDER_HT}${i}§`, tag);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
431
515
|
// ────────────────────────────────────────────────────────────────
|
|
432
516
|
// Phase 3 — Block Scanning + Inline Formatting + Paragraphs
|
|
433
517
|
// ────────────────────────────────────────────────────────────────
|
|
@@ -469,7 +553,6 @@ function quikdown(markdown, options = {}) {
|
|
|
469
553
|
// Images (must come before links —  vs [text](url))
|
|
470
554
|
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
|
471
555
|
const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);
|
|
472
|
-
// Bidirectional attributes are only exercised via quikdown_bd bundle.
|
|
473
556
|
/* istanbul ignore next - bd-only branch */
|
|
474
557
|
const altAttr = bidirectional && alt ? ` data-qd-alt="${escapeHtml(alt)}"` : '';
|
|
475
558
|
/* istanbul ignore next - bd-only branch */
|
|
@@ -493,8 +576,12 @@ function quikdown(markdown, options = {}) {
|
|
|
493
576
|
return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
|
|
494
577
|
});
|
|
495
578
|
|
|
579
|
+
// Protect rendered tags so emphasis regexes don't see attribute
|
|
580
|
+
// values — fixes #3 (underscores in URLs interpreted as emphasis).
|
|
581
|
+
const savedTags = [];
|
|
582
|
+
html = html.replace(/<[^>]+>/g, m => { savedTags.push(m); return `%%T${savedTags.length - 1}%%`; });
|
|
583
|
+
|
|
496
584
|
// Bold, italic, strikethrough
|
|
497
|
-
// Order matters: ** before * (so ** isn't consumed as two *s)
|
|
498
585
|
const inlinePatterns = [
|
|
499
586
|
[/\*\*(.+?)\*\*/g, 'strong', '**'],
|
|
500
587
|
[/__(.+?)__/g, 'strong', '__'],
|
|
@@ -506,6 +593,9 @@ function quikdown(markdown, options = {}) {
|
|
|
506
593
|
html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);
|
|
507
594
|
});
|
|
508
595
|
|
|
596
|
+
// Restore protected tags
|
|
597
|
+
html = html.replace(/%%T(\d+)%%/g, (_, i) => savedTags[i]);
|
|
598
|
+
|
|
509
599
|
// ── Step 5: Line breaks + paragraph wrapping ──
|
|
510
600
|
if (lazy_linefeeds) {
|
|
511
601
|
// Lazy linefeeds mode: every single \n becomes <br> EXCEPT:
|
|
@@ -663,6 +753,14 @@ function scanLineBlocks(text, getAttr, dataQd) {
|
|
|
663
753
|
while (i < lines.length) {
|
|
664
754
|
const line = lines[i];
|
|
665
755
|
|
|
756
|
+
// ── Markdown comment (reference-link hack) ──
|
|
757
|
+
// [//]: # (comment) or [//]: # "comment" or [//]: #
|
|
758
|
+
// These produce no output — standard markdown comment convention.
|
|
759
|
+
if (/^\[\/\/\]: #/.test(line)) {
|
|
760
|
+
i++;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
|
|
666
764
|
// ── Heading ──
|
|
667
765
|
// Count leading '#' characters. Valid heading: 1-6 hashes then a space.
|
|
668
766
|
// Example: "## Hello World ##" → <h2>Hello World</h2>
|
|
@@ -1027,6 +1125,7 @@ quikdown.configure = function(options) {
|
|
|
1027
1125
|
/** Semantic version (injected at build time) */
|
|
1028
1126
|
quikdown.version = quikdownVersion;
|
|
1029
1127
|
|
|
1128
|
+
|
|
1030
1129
|
// ════════════════════════════════════════════════════════════════════
|
|
1031
1130
|
// Exports
|
|
1032
1131
|
// ════════════════════════════════════════════════════════════════════
|
|
@@ -2871,7 +2970,7 @@ async function getRenderedContent(previewPanel) {
|
|
|
2871
2970
|
if (copyToClipboard(fragment)) {
|
|
2872
2971
|
return { success: true, html: htmlContent, text };
|
|
2873
2972
|
}
|
|
2874
|
-
throw new Error('Fallback copy failed');
|
|
2973
|
+
throw new Error('Fallback copy failed', { cause: modernErr });
|
|
2875
2974
|
}
|
|
2876
2975
|
} else {
|
|
2877
2976
|
// Windows/Linux approach (like squibview)
|
|
@@ -2901,7 +3000,7 @@ async function getRenderedContent(previewPanel) {
|
|
|
2901
3000
|
|
|
2902
3001
|
const successful = document.execCommand('copy');
|
|
2903
3002
|
if (!successful) {
|
|
2904
|
-
throw new Error('Fallback copy failed');
|
|
3003
|
+
throw new Error('Fallback copy failed', { cause: modernErr });
|
|
2905
3004
|
}
|
|
2906
3005
|
return { success: true, html: htmlContent, text };
|
|
2907
3006
|
} finally {
|
|
@@ -2924,6 +3023,37 @@ async function getRenderedContent(previewPanel) {
|
|
|
2924
3023
|
*/
|
|
2925
3024
|
|
|
2926
3025
|
|
|
3026
|
+
/**
|
|
3027
|
+
* Curated safe HTML tag whitelist.
|
|
3028
|
+
* Pass to quikdown's `allow_unsafe_html` option to allow these tags
|
|
3029
|
+
* through while escaping everything else. Callers can use this as-is,
|
|
3030
|
+
* pass a subset, or build their own object — any object whose keys are
|
|
3031
|
+
* lowercase tag names works.
|
|
3032
|
+
*
|
|
3033
|
+
* @example
|
|
3034
|
+
* // Use the curated list
|
|
3035
|
+
* quikdown(md, { allow_unsafe_html: QuikdownEditor.SAFE_HTML_TAGS });
|
|
3036
|
+
*
|
|
3037
|
+
* // Or a minimal subset
|
|
3038
|
+
* quikdown(md, { allow_unsafe_html: { img: 1, a: 1, br: 1 } });
|
|
3039
|
+
*
|
|
3040
|
+
* // Or an array (converted internally)
|
|
3041
|
+
* quikdown(md, { allow_unsafe_html: ['img', 'a', 'br'] });
|
|
3042
|
+
*/
|
|
3043
|
+
const SAFE_HTML_TAGS = {
|
|
3044
|
+
b:1, i:1, em:1, strong:1, del:1, s:1, u:1, mark:1, sup:1, sub:1,
|
|
3045
|
+
kbd:1, abbr:1, var:1, samp:1, cite:1, small:1, ins:1, dfn:1,
|
|
3046
|
+
ruby:1, rt:1, rp:1, time:1, wbr:1,
|
|
3047
|
+
img:1, picture:1, source:1, video:1, audio:1, figure:1, figcaption:1,
|
|
3048
|
+
a:1, br:1, hr:1,
|
|
3049
|
+
div:1, span:1, p:1, details:1, summary:1,
|
|
3050
|
+
section:1, article:1, aside:1, header:1, footer:1, nav:1, main:1,
|
|
3051
|
+
table:1, thead:1, tbody:1, tfoot:1, tr:1, th:1, td:1, caption:1, col:1, colgroup:1,
|
|
3052
|
+
ul:1, ol:1, li:1, dl:1, dt:1, dd:1,
|
|
3053
|
+
h1:1, h2:1, h3:1, h4:1, h5:1, h6:1,
|
|
3054
|
+
blockquote:1, pre:1, code:1
|
|
3055
|
+
};
|
|
3056
|
+
|
|
2927
3057
|
// Default options
|
|
2928
3058
|
const DEFAULT_OPTIONS = {
|
|
2929
3059
|
mode: 'split', // 'source' | 'preview' | 'split'
|
|
@@ -2963,7 +3093,9 @@ const DEFAULT_OPTIONS = {
|
|
|
2963
3093
|
customFences: {}, // { 'language': (code, lang) => html }
|
|
2964
3094
|
enableComplexFences: true, // Enable CSV tables, math rendering, SVG, etc.
|
|
2965
3095
|
showUndoRedo: false, // Show undo/redo toolbar buttons
|
|
2966
|
-
undoStackSize: 100
|
|
3096
|
+
undoStackSize: 100, // Maximum number of undo states to keep
|
|
3097
|
+
allowUnsafeHTML: false, // false | 'limited' | true — controls HTML passthrough
|
|
3098
|
+
showAllowUnsafeHTML: false // Show toolbar button to cycle HTML mode
|
|
2967
3099
|
};
|
|
2968
3100
|
|
|
2969
3101
|
// Library catalog used by preloadFences. Each entry knows how to:
|
|
@@ -3208,7 +3340,17 @@ class QuikdownEditor {
|
|
|
3208
3340
|
lazyLFBtn.title = 'Convert single newlines to paragraph breaks (one-time transform)';
|
|
3209
3341
|
toolbar.appendChild(lazyLFBtn);
|
|
3210
3342
|
}
|
|
3211
|
-
|
|
3343
|
+
|
|
3344
|
+
// Allow unsafe HTML toggle button (if enabled)
|
|
3345
|
+
if (this.options.showAllowUnsafeHTML) {
|
|
3346
|
+
const htmlModeBtn = document.createElement('button');
|
|
3347
|
+
htmlModeBtn.className = 'qde-btn';
|
|
3348
|
+
htmlModeBtn.dataset.action = 'toggle-html-mode';
|
|
3349
|
+
htmlModeBtn.textContent = this._getHtmlModeLabel(this.options.allowUnsafeHTML);
|
|
3350
|
+
htmlModeBtn.title = this._getHtmlModeTooltip(this.options.allowUnsafeHTML);
|
|
3351
|
+
toolbar.appendChild(htmlModeBtn);
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3212
3354
|
return toolbar;
|
|
3213
3355
|
}
|
|
3214
3356
|
|
|
@@ -3267,6 +3409,25 @@ class QuikdownEditor {
|
|
|
3267
3409
|
opacity: 0.4;
|
|
3268
3410
|
pointer-events: none;
|
|
3269
3411
|
}
|
|
3412
|
+
.qde-btn[data-action="toggle-html-mode"] {
|
|
3413
|
+
position: relative;
|
|
3414
|
+
}
|
|
3415
|
+
.qde-btn[data-action="toggle-html-mode"]:hover::after {
|
|
3416
|
+
content: attr(title);
|
|
3417
|
+
position: absolute;
|
|
3418
|
+
bottom: calc(100% + 6px);
|
|
3419
|
+
left: 50%;
|
|
3420
|
+
transform: translateX(-50%);
|
|
3421
|
+
padding: 5px 10px;
|
|
3422
|
+
background: #1f2937;
|
|
3423
|
+
color: #fff;
|
|
3424
|
+
font-size: 0.75rem;
|
|
3425
|
+
font-weight: 400;
|
|
3426
|
+
white-space: nowrap;
|
|
3427
|
+
border-radius: 4px;
|
|
3428
|
+
pointer-events: none;
|
|
3429
|
+
z-index: 10;
|
|
3430
|
+
}
|
|
3270
3431
|
|
|
3271
3432
|
.qde-spacer {
|
|
3272
3433
|
flex: 1;
|
|
@@ -3371,6 +3532,9 @@ class QuikdownEditor {
|
|
|
3371
3532
|
.qde-preview > svg {
|
|
3372
3533
|
max-width: 100%;
|
|
3373
3534
|
}
|
|
3535
|
+
.qde-preview img {
|
|
3536
|
+
display: inline;
|
|
3537
|
+
}
|
|
3374
3538
|
.qde-preview .leaflet-container { box-sizing: border-box; }
|
|
3375
3539
|
|
|
3376
3540
|
/* Standard markdown tables (the .quikdown-table class) need to
|
|
@@ -3791,10 +3955,16 @@ class QuikdownEditor {
|
|
|
3791
3955
|
this.previewPanel.innerHTML = '<div style="color: #999; font-style: italic; padding: 16px;">Start typing markdown in the source panel...</div>';
|
|
3792
3956
|
}
|
|
3793
3957
|
} else {
|
|
3958
|
+
// Translate editor's allowUnsafeHTML to parser's allow_unsafe_html:
|
|
3959
|
+
// false → false, true → true, 'limited' → quikdown_bd.SAFE_HTML_TAGS
|
|
3960
|
+
const htmlMode = this.options.allowUnsafeHTML;
|
|
3961
|
+
const allowHtml = htmlMode === 'limited' ? SAFE_HTML_TAGS : htmlMode;
|
|
3962
|
+
|
|
3794
3963
|
this._html = quikdown_bd(markdown, {
|
|
3795
3964
|
fence_plugin: this.createFencePlugin(),
|
|
3796
3965
|
lazy_linefeeds: this.options.lazy_linefeeds,
|
|
3797
|
-
inline_styles: this.options.inline_styles
|
|
3966
|
+
inline_styles: this.options.inline_styles,
|
|
3967
|
+
allow_unsafe_html: allowHtml
|
|
3798
3968
|
});
|
|
3799
3969
|
|
|
3800
3970
|
// Update preview if visible
|
|
@@ -4006,8 +4176,8 @@ class QuikdownEditor {
|
|
|
4006
4176
|
const reverse = (element) => {
|
|
4007
4177
|
// Get the language from data attribute
|
|
4008
4178
|
const lang = element.getAttribute('data-qd-lang') || '';
|
|
4009
|
-
let content
|
|
4010
|
-
|
|
4179
|
+
let content;
|
|
4180
|
+
|
|
4011
4181
|
// For syntax-highlighted code, extract the raw text
|
|
4012
4182
|
if (element.querySelector('code.hljs')) {
|
|
4013
4183
|
const code = element.querySelector('code.hljs');
|
|
@@ -5028,6 +5198,9 @@ class QuikdownEditor {
|
|
|
5028
5198
|
case 'redo':
|
|
5029
5199
|
this.redo();
|
|
5030
5200
|
break;
|
|
5201
|
+
case 'toggle-html-mode':
|
|
5202
|
+
this.cycleAllowUnsafeHTML();
|
|
5203
|
+
break;
|
|
5031
5204
|
}
|
|
5032
5205
|
}
|
|
5033
5206
|
|
|
@@ -5382,6 +5555,62 @@ class QuikdownEditor {
|
|
|
5382
5555
|
}
|
|
5383
5556
|
}
|
|
5384
5557
|
|
|
5558
|
+
/**
|
|
5559
|
+
* Get the label for the current HTML passthrough mode.
|
|
5560
|
+
* @private
|
|
5561
|
+
*/
|
|
5562
|
+
_getHtmlModeLabel(mode) {
|
|
5563
|
+
if (mode === true) return 'HTML: Raw';
|
|
5564
|
+
if (mode === 'limited') return 'HTML: Safe';
|
|
5565
|
+
return 'HTML: Off';
|
|
5566
|
+
}
|
|
5567
|
+
|
|
5568
|
+
/** @private */
|
|
5569
|
+
_getHtmlModeTooltip(mode) {
|
|
5570
|
+
if (mode === true) return 'All HTML passes through — no protection';
|
|
5571
|
+
if (mode === 'limited') return 'Safe tags render, dangerous tags escaped';
|
|
5572
|
+
return 'All HTML tags shown as text';
|
|
5573
|
+
}
|
|
5574
|
+
|
|
5575
|
+
/**
|
|
5576
|
+
* Cycle allowUnsafeHTML through false → "limited" → true → false.
|
|
5577
|
+
*/
|
|
5578
|
+
cycleAllowUnsafeHTML() {
|
|
5579
|
+
const current = this.options.allowUnsafeHTML;
|
|
5580
|
+
let next;
|
|
5581
|
+
if (current === false) next = 'limited';
|
|
5582
|
+
else if (current === 'limited') next = true;
|
|
5583
|
+
else next = false;
|
|
5584
|
+
this.setAllowUnsafeHTML(next);
|
|
5585
|
+
}
|
|
5586
|
+
|
|
5587
|
+
/**
|
|
5588
|
+
* Set the HTML passthrough mode.
|
|
5589
|
+
* @param {boolean|'limited'} mode - false, 'limited', or true
|
|
5590
|
+
*/
|
|
5591
|
+
setAllowUnsafeHTML(mode) {
|
|
5592
|
+
if (mode !== false && mode !== true && mode !== 'limited') return;
|
|
5593
|
+
this.options.allowUnsafeHTML = mode;
|
|
5594
|
+
// Update toolbar button label
|
|
5595
|
+
if (this.toolbar) {
|
|
5596
|
+
const btn = this.toolbar.querySelector('[data-action="toggle-html-mode"]');
|
|
5597
|
+
if (btn) {
|
|
5598
|
+
btn.textContent = this._getHtmlModeLabel(mode);
|
|
5599
|
+
btn.title = this._getHtmlModeTooltip(mode);
|
|
5600
|
+
}
|
|
5601
|
+
}
|
|
5602
|
+
// Re-render
|
|
5603
|
+
this.updateFromMarkdown(this._markdown);
|
|
5604
|
+
}
|
|
5605
|
+
|
|
5606
|
+
/**
|
|
5607
|
+
* Get the current HTML passthrough mode.
|
|
5608
|
+
* @returns {boolean|'limited'}
|
|
5609
|
+
*/
|
|
5610
|
+
getAllowUnsafeHTML() {
|
|
5611
|
+
return this.options.allowUnsafeHTML;
|
|
5612
|
+
}
|
|
5613
|
+
|
|
5385
5614
|
/**
|
|
5386
5615
|
* Destroy the editor
|
|
5387
5616
|
*/
|
|
@@ -5402,6 +5631,9 @@ class QuikdownEditor {
|
|
|
5402
5631
|
}
|
|
5403
5632
|
}
|
|
5404
5633
|
|
|
5634
|
+
/** Static: curated safe HTML tag whitelist for allow_unsafe_html */
|
|
5635
|
+
QuikdownEditor.SAFE_HTML_TAGS = SAFE_HTML_TAGS;
|
|
5636
|
+
|
|
5405
5637
|
// Export for CommonJS (needed for bundled ESM to work with Jest)
|
|
5406
5638
|
if (typeof module !== 'undefined' && module.exports) {
|
|
5407
5639
|
module.exports = QuikdownEditor;
|