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
@@ -1062,9 +1062,9 @@ var quikchat = /*#__PURE__*/function () {
1062
1062
  key: "version",
1063
1063
  value: function version() {
1064
1064
  return {
1065
- "version": "1.2.6",
1065
+ "version": "1.2.7",
1066
1066
  "license": "BSD-2",
1067
- "url": "https://github/deftio/quikchat"
1067
+ "url": "https://github.com/deftio/quikchat"
1068
1068
  };
1069
1069
  }
1070
1070
 
@@ -1122,7 +1122,7 @@ var quikchat = /*#__PURE__*/function () {
1122
1122
 
1123
1123
  /**
1124
1124
  * quikdown_bd - Bidirectional Markdown Parser
1125
- * @version 1.2.10
1125
+ * @version 1.2.12
1126
1126
  * @license BSD-2-Clause
1127
1127
  * @copyright DeftIO 2025
1128
1128
  */
@@ -1234,7 +1234,7 @@ function isDashHRLine(trimmed) {
1234
1234
  // ────────────────────────────────────────────────────────────────────
1235
1235
 
1236
1236
  /** Build-time version stamp (injected by tools/updateVersion) */
1237
- const quikdownVersion = '1.2.10';
1237
+ const quikdownVersion = '1.2.12';
1238
1238
 
1239
1239
  /** CSS class prefix used for all generated elements */
1240
1240
  const CLASS_PREFIX = 'quikdown-';
@@ -1242,6 +1242,10 @@ const CLASS_PREFIX = 'quikdown-';
1242
1242
  /** Placeholder sigils — chosen to be extremely unlikely in real text */
1243
1243
  const PLACEHOLDER_CB = '§CB'; // fenced code blocks
1244
1244
  const PLACEHOLDER_IC = '§IC'; // inline code spans
1245
+ const PLACEHOLDER_HT = '§HT'; // safe HTML tags (limited mode)
1246
+
1247
+ /** Attributes whose values need URL sanitization */
1248
+ const URL_ATTRIBUTES = { href:1, src:1, action:1, formaction:1 };
1245
1249
 
1246
1250
  /** HTML entity escape map */
1247
1251
  const ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'};
@@ -1376,6 +1380,46 @@ function quikdown(markdown, options = {}) {
1376
1380
  return trimmedUrl;
1377
1381
  }
1378
1382
 
1383
+ /**
1384
+ * Sanitize attributes on an HTML tag string for limited mode.
1385
+ * Strips on* event handlers (case-insensitive) and runs sanitizeUrl()
1386
+ * on href/src/action/formaction values.
1387
+ */
1388
+ function sanitizeHtmlTagAttrs(tagStr) {
1389
+ // Self-closing or void tag without attributes — pass through
1390
+ if (!/\s/.test(tagStr.replace(/<\/?[a-zA-Z][a-zA-Z0-9]*/, '').replace(/\/?>$/, ''))) {
1391
+ return tagStr;
1392
+ }
1393
+ // Parse: <tagname ...attrs... > or <tagname ...attrs... />
1394
+ const m = tagStr.match(/^(<\/?[a-zA-Z][a-zA-Z0-9]*)([\s\S]*?)(\/?>)$/);
1395
+ /* istanbul ignore next - defensive: Phase 1.5 regex guarantees valid tag shape */
1396
+ if (!m) return tagStr;
1397
+
1398
+ const [, open, attrStr, close] = m;
1399
+ // Match individual attributes: name="value", name='value', name=value, or bare name
1400
+ // eslint-disable-next-line security/detect-unsafe-regex -- linear: no nested quantifiers
1401
+ const attrRe = /([a-zA-Z_][\w\-.:]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
1402
+ const attrs = [];
1403
+ let am;
1404
+ while ((am = attrRe.exec(attrStr)) !== null) {
1405
+ const name = am[1];
1406
+ const value = am[2] !== undefined ? am[2] : am[3] !== undefined ? am[3] : am[4];
1407
+ // Strip event handlers (on*)
1408
+ if (/^on/i.test(name)) continue;
1409
+ if (value === undefined) {
1410
+ // Boolean attribute (e.g. disabled, checked)
1411
+ attrs.push(name);
1412
+ } else {
1413
+ let sanitized = value;
1414
+ if (name.toLowerCase() in URL_ATTRIBUTES) {
1415
+ sanitized = sanitizeUrl(value);
1416
+ }
1417
+ attrs.push(`${name}="${sanitized}"`);
1418
+ }
1419
+ }
1420
+ return open + (attrs.length ? ' ' + attrs.join(' ') : '') + close;
1421
+ }
1422
+
1379
1423
  // ────────────────────────────────────────────────────────────────
1380
1424
  // Phase 1 — Code Extraction
1381
1425
  // ────────────────────────────────────────────────────────────────
@@ -1427,17 +1471,57 @@ function quikdown(markdown, options = {}) {
1427
1471
  return placeholder;
1428
1472
  });
1429
1473
 
1474
+ // ────────────────────────────────────────────────────────────────
1475
+ // Phase 1.5 — Safe HTML Extraction (whitelist mode)
1476
+ // ────────────────────────────────────────────────────────────────
1477
+ // When allow_unsafe_html is an object or array, extract whitelisted
1478
+ // HTML tags, sanitize their attributes, and replace with placeholders.
1479
+ // Non-whitelisted tags stay in text so Phase 2 will escape them.
1480
+
1481
+ const safeTags = [];
1482
+ // Normalize: array → object for O(1) lookup; object used as-is
1483
+ const htmlAllow = Array.isArray(allow_unsafe_html)
1484
+ ? Object.fromEntries(allow_unsafe_html.map(t => [t, 1]))
1485
+ : (allow_unsafe_html && typeof allow_unsafe_html === 'object') ? allow_unsafe_html : null;
1486
+
1487
+ if (htmlAllow) {
1488
+ // Pass through HTML comments — browsers render them as nothing
1489
+ html = html.replace(/<!--[\s\S]*?-->/g, (match) => {
1490
+ const idx = safeTags.length;
1491
+ safeTags.push(match);
1492
+ return `${PLACEHOLDER_HT}${idx}§`;
1493
+ });
1494
+ html = html.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/g, (match, tagName) => {
1495
+ if (tagName.toLowerCase() in htmlAllow) {
1496
+ const sanitized = sanitizeHtmlTagAttrs(match);
1497
+ const idx = safeTags.length;
1498
+ safeTags.push(sanitized);
1499
+ return `${PLACEHOLDER_HT}${idx}§`;
1500
+ }
1501
+ // Not whitelisted — leave in text for Phase 2 to escape
1502
+ return match;
1503
+ });
1504
+ }
1505
+
1430
1506
  // ────────────────────────────────────────────────────────────────
1431
1507
  // Phase 2 — HTML Escaping
1432
1508
  // ────────────────────────────────────────────────────────────────
1433
1509
  // All remaining text (everything except code placeholders) is escaped
1434
1510
  // to prevent XSS. The `allow_unsafe_html` option skips this for
1435
1511
  // trusted pipelines that intentionally embed raw HTML.
1512
+ // For whitelist mode, escaping still runs (only `true` bypasses it).
1436
1513
 
1437
- if (!allow_unsafe_html) {
1514
+ if (allow_unsafe_html !== true) {
1438
1515
  html = escapeHtml(html);
1439
1516
  }
1440
1517
 
1518
+ // Restore safe HTML tag placeholders after escaping
1519
+ if (htmlAllow) {
1520
+ safeTags.forEach((tag, i) => {
1521
+ html = html.replace(`${PLACEHOLDER_HT}${i}§`, tag);
1522
+ });
1523
+ }
1524
+
1441
1525
  // ────────────────────────────────────────────────────────────────
1442
1526
  // Phase 3 — Block Scanning + Inline Formatting + Paragraphs
1443
1527
  // ────────────────────────────────────────────────────────────────
@@ -1679,6 +1763,14 @@ function scanLineBlocks(text, getAttr, dataQd) {
1679
1763
  while (i < lines.length) {
1680
1764
  const line = lines[i];
1681
1765
 
1766
+ // ── Markdown comment (reference-link hack) ──
1767
+ // [//]: # (comment) or [//]: # "comment" or [//]: #
1768
+ // These produce no output — standard markdown comment convention.
1769
+ if (/^\[\/\/\]: #/.test(line)) {
1770
+ i++;
1771
+ continue;
1772
+ }
1773
+
1682
1774
  // ── Heading ──
1683
1775
  // Count leading '#' characters. Valid heading: 1-6 hashes then a space.
1684
1776
  // Example: "## Hello World ##" → <h2>Hello World</h2>
@@ -2043,6 +2135,7 @@ quikdown.configure = function(options) {
2043
2135
  /** Semantic version (injected at build time) */
2044
2136
  quikdown.version = quikdownVersion;
2045
2137
 
2138
+
2046
2139
  // ════════════════════════════════════════════════════════════════════
2047
2140
  // Exports
2048
2141
  // ════════════════════════════════════════════════════════════════════