quikchat 1.2.6 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +7 -2
  2. package/dist/build-manifest.json +57 -57
  3. package/dist/quikchat-md-full.cjs.js +98 -5
  4. package/dist/quikchat-md-full.cjs.js.map +1 -1
  5. package/dist/quikchat-md-full.cjs.min.js +3 -3
  6. package/dist/quikchat-md-full.cjs.min.js.map +1 -1
  7. package/dist/quikchat-md-full.esm.js +98 -5
  8. package/dist/quikchat-md-full.esm.js.map +1 -1
  9. package/dist/quikchat-md-full.esm.min.js +3 -3
  10. package/dist/quikchat-md-full.esm.min.js.map +1 -1
  11. package/dist/quikchat-md-full.umd.js +98 -5
  12. package/dist/quikchat-md-full.umd.js.map +1 -1
  13. package/dist/quikchat-md-full.umd.min.js +3 -3
  14. package/dist/quikchat-md-full.umd.min.js.map +1 -1
  15. package/dist/quikchat-md.cjs.js +98 -5
  16. package/dist/quikchat-md.cjs.js.map +1 -1
  17. package/dist/quikchat-md.cjs.min.js +3 -3
  18. package/dist/quikchat-md.cjs.min.js.map +1 -1
  19. package/dist/quikchat-md.esm.js +98 -5
  20. package/dist/quikchat-md.esm.js.map +1 -1
  21. package/dist/quikchat-md.esm.min.js +3 -3
  22. package/dist/quikchat-md.esm.min.js.map +1 -1
  23. package/dist/quikchat-md.umd.js +98 -5
  24. package/dist/quikchat-md.umd.js.map +1 -1
  25. package/dist/quikchat-md.umd.min.js +3 -3
  26. package/dist/quikchat-md.umd.min.js.map +1 -1
  27. package/dist/quikchat.cjs.js +2 -2
  28. package/dist/quikchat.cjs.js.map +1 -1
  29. package/dist/quikchat.cjs.min.js +1 -1
  30. package/dist/quikchat.cjs.min.js.map +1 -1
  31. package/dist/quikchat.esm.js +2 -2
  32. package/dist/quikchat.esm.js.map +1 -1
  33. package/dist/quikchat.esm.min.js +1 -1
  34. package/dist/quikchat.esm.min.js.map +1 -1
  35. package/dist/quikchat.umd.js +2 -2
  36. package/dist/quikchat.umd.js.map +1 -1
  37. package/dist/quikchat.umd.min.js +1 -1
  38. package/dist/quikchat.umd.min.js.map +1 -1
  39. package/package.json +2 -2
@@ -1068,9 +1068,9 @@
1068
1068
  key: "version",
1069
1069
  value: function version() {
1070
1070
  return {
1071
- "version": "1.2.6",
1071
+ "version": "1.2.7",
1072
1072
  "license": "BSD-2",
1073
- "url": "https://github/deftio/quikchat"
1073
+ "url": "https://github.com/deftio/quikchat"
1074
1074
  };
1075
1075
  }
1076
1076
 
@@ -1128,7 +1128,7 @@
1128
1128
 
1129
1129
  /**
1130
1130
  * quikdown_bd - Bidirectional Markdown Parser
1131
- * @version 1.2.10
1131
+ * @version 1.2.12
1132
1132
  * @license BSD-2-Clause
1133
1133
  * @copyright DeftIO 2025
1134
1134
  */
@@ -1240,7 +1240,7 @@
1240
1240
  // ────────────────────────────────────────────────────────────────────
1241
1241
 
1242
1242
  /** Build-time version stamp (injected by tools/updateVersion) */
1243
- const quikdownVersion = '1.2.10';
1243
+ const quikdownVersion = '1.2.12';
1244
1244
 
1245
1245
  /** CSS class prefix used for all generated elements */
1246
1246
  const CLASS_PREFIX = 'quikdown-';
@@ -1248,6 +1248,10 @@
1248
1248
  /** Placeholder sigils — chosen to be extremely unlikely in real text */
1249
1249
  const PLACEHOLDER_CB = '§CB'; // fenced code blocks
1250
1250
  const PLACEHOLDER_IC = '§IC'; // inline code spans
1251
+ const PLACEHOLDER_HT = '§HT'; // safe HTML tags (limited mode)
1252
+
1253
+ /** Attributes whose values need URL sanitization */
1254
+ const URL_ATTRIBUTES = { href:1, src:1, action:1, formaction:1 };
1251
1255
 
1252
1256
  /** HTML entity escape map */
1253
1257
  const ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'};
@@ -1382,6 +1386,46 @@
1382
1386
  return trimmedUrl;
1383
1387
  }
1384
1388
 
1389
+ /**
1390
+ * Sanitize attributes on an HTML tag string for limited mode.
1391
+ * Strips on* event handlers (case-insensitive) and runs sanitizeUrl()
1392
+ * on href/src/action/formaction values.
1393
+ */
1394
+ function sanitizeHtmlTagAttrs(tagStr) {
1395
+ // Self-closing or void tag without attributes — pass through
1396
+ if (!/\s/.test(tagStr.replace(/<\/?[a-zA-Z][a-zA-Z0-9]*/, '').replace(/\/?>$/, ''))) {
1397
+ return tagStr;
1398
+ }
1399
+ // Parse: <tagname ...attrs... > or <tagname ...attrs... />
1400
+ const m = tagStr.match(/^(<\/?[a-zA-Z][a-zA-Z0-9]*)([\s\S]*?)(\/?>)$/);
1401
+ /* istanbul ignore next - defensive: Phase 1.5 regex guarantees valid tag shape */
1402
+ if (!m) return tagStr;
1403
+
1404
+ const [, open, attrStr, close] = m;
1405
+ // Match individual attributes: name="value", name='value', name=value, or bare name
1406
+ // eslint-disable-next-line security/detect-unsafe-regex -- linear: no nested quantifiers
1407
+ const attrRe = /([a-zA-Z_][\w\-.:]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
1408
+ const attrs = [];
1409
+ let am;
1410
+ while ((am = attrRe.exec(attrStr)) !== null) {
1411
+ const name = am[1];
1412
+ const value = am[2] !== undefined ? am[2] : am[3] !== undefined ? am[3] : am[4];
1413
+ // Strip event handlers (on*)
1414
+ if (/^on/i.test(name)) continue;
1415
+ if (value === undefined) {
1416
+ // Boolean attribute (e.g. disabled, checked)
1417
+ attrs.push(name);
1418
+ } else {
1419
+ let sanitized = value;
1420
+ if (name.toLowerCase() in URL_ATTRIBUTES) {
1421
+ sanitized = sanitizeUrl(value);
1422
+ }
1423
+ attrs.push(`${name}="${sanitized}"`);
1424
+ }
1425
+ }
1426
+ return open + (attrs.length ? ' ' + attrs.join(' ') : '') + close;
1427
+ }
1428
+
1385
1429
  // ────────────────────────────────────────────────────────────────
1386
1430
  // Phase 1 — Code Extraction
1387
1431
  // ────────────────────────────────────────────────────────────────
@@ -1433,17 +1477,57 @@
1433
1477
  return placeholder;
1434
1478
  });
1435
1479
 
1480
+ // ────────────────────────────────────────────────────────────────
1481
+ // Phase 1.5 — Safe HTML Extraction (whitelist mode)
1482
+ // ────────────────────────────────────────────────────────────────
1483
+ // When allow_unsafe_html is an object or array, extract whitelisted
1484
+ // HTML tags, sanitize their attributes, and replace with placeholders.
1485
+ // Non-whitelisted tags stay in text so Phase 2 will escape them.
1486
+
1487
+ const safeTags = [];
1488
+ // Normalize: array → object for O(1) lookup; object used as-is
1489
+ const htmlAllow = Array.isArray(allow_unsafe_html)
1490
+ ? Object.fromEntries(allow_unsafe_html.map(t => [t, 1]))
1491
+ : (allow_unsafe_html && typeof allow_unsafe_html === 'object') ? allow_unsafe_html : null;
1492
+
1493
+ if (htmlAllow) {
1494
+ // Pass through HTML comments — browsers render them as nothing
1495
+ html = html.replace(/<!--[\s\S]*?-->/g, (match) => {
1496
+ const idx = safeTags.length;
1497
+ safeTags.push(match);
1498
+ return `${PLACEHOLDER_HT}${idx}§`;
1499
+ });
1500
+ html = html.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/g, (match, tagName) => {
1501
+ if (tagName.toLowerCase() in htmlAllow) {
1502
+ const sanitized = sanitizeHtmlTagAttrs(match);
1503
+ const idx = safeTags.length;
1504
+ safeTags.push(sanitized);
1505
+ return `${PLACEHOLDER_HT}${idx}§`;
1506
+ }
1507
+ // Not whitelisted — leave in text for Phase 2 to escape
1508
+ return match;
1509
+ });
1510
+ }
1511
+
1436
1512
  // ────────────────────────────────────────────────────────────────
1437
1513
  // Phase 2 — HTML Escaping
1438
1514
  // ────────────────────────────────────────────────────────────────
1439
1515
  // All remaining text (everything except code placeholders) is escaped
1440
1516
  // to prevent XSS. The `allow_unsafe_html` option skips this for
1441
1517
  // trusted pipelines that intentionally embed raw HTML.
1518
+ // For whitelist mode, escaping still runs (only `true` bypasses it).
1442
1519
 
1443
- if (!allow_unsafe_html) {
1520
+ if (allow_unsafe_html !== true) {
1444
1521
  html = escapeHtml(html);
1445
1522
  }
1446
1523
 
1524
+ // Restore safe HTML tag placeholders after escaping
1525
+ if (htmlAllow) {
1526
+ safeTags.forEach((tag, i) => {
1527
+ html = html.replace(`${PLACEHOLDER_HT}${i}§`, tag);
1528
+ });
1529
+ }
1530
+
1447
1531
  // ────────────────────────────────────────────────────────────────
1448
1532
  // Phase 3 — Block Scanning + Inline Formatting + Paragraphs
1449
1533
  // ────────────────────────────────────────────────────────────────
@@ -1685,6 +1769,14 @@
1685
1769
  while (i < lines.length) {
1686
1770
  const line = lines[i];
1687
1771
 
1772
+ // ── Markdown comment (reference-link hack) ──
1773
+ // [//]: # (comment) or [//]: # "comment" or [//]: #
1774
+ // These produce no output — standard markdown comment convention.
1775
+ if (/^\[\/\/\]: #/.test(line)) {
1776
+ i++;
1777
+ continue;
1778
+ }
1779
+
1688
1780
  // ── Heading ──
1689
1781
  // Count leading '#' characters. Valid heading: 1-6 hashes then a space.
1690
1782
  // Example: "## Hello World ##" → <h2>Hello World</h2>
@@ -2049,6 +2141,7 @@
2049
2141
  /** Semantic version (injected at build time) */
2050
2142
  quikdown.version = quikdownVersion;
2051
2143
 
2144
+
2052
2145
  // ════════════════════════════════════════════════════════════════════
2053
2146
  // Exports
2054
2147
  // ════════════════════════════════════════════════════════════════════