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