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
@@ -904,9 +904,9 @@
904
904
  key: "version",
905
905
  value: function version() {
906
906
  return {
907
- "version": "1.2.6",
907
+ "version": "1.2.7",
908
908
  "license": "BSD-2",
909
- "url": "https://github/deftio/quikchat"
909
+ "url": "https://github.com/deftio/quikchat"
910
910
  };
911
911
  }
912
912
 
@@ -964,7 +964,7 @@
964
964
 
965
965
  /**
966
966
  * quikdown - Lightweight Markdown Parser
967
- * @version 1.2.10
967
+ * @version 1.2.12
968
968
  * @license BSD-2-Clause
969
969
  * @copyright DeftIO 2025
970
970
  */
@@ -1076,7 +1076,7 @@
1076
1076
  // ────────────────────────────────────────────────────────────────────
1077
1077
 
1078
1078
  /** Build-time version stamp (injected by tools/updateVersion) */
1079
- const quikdownVersion = '1.2.10';
1079
+ const quikdownVersion = '1.2.12';
1080
1080
 
1081
1081
  /** CSS class prefix used for all generated elements */
1082
1082
  const CLASS_PREFIX = 'quikdown-';
@@ -1084,6 +1084,10 @@
1084
1084
  /** Placeholder sigils — chosen to be extremely unlikely in real text */
1085
1085
  const PLACEHOLDER_CB = '§CB'; // fenced code blocks
1086
1086
  const PLACEHOLDER_IC = '§IC'; // inline code spans
1087
+ const PLACEHOLDER_HT = '§HT'; // safe HTML tags (limited mode)
1088
+
1089
+ /** Attributes whose values need URL sanitization */
1090
+ const URL_ATTRIBUTES = { href:1, src:1, action:1, formaction:1 };
1087
1091
 
1088
1092
  /** HTML entity escape map */
1089
1093
  const ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'};
@@ -1218,6 +1222,46 @@
1218
1222
  return trimmedUrl;
1219
1223
  }
1220
1224
 
1225
+ /**
1226
+ * Sanitize attributes on an HTML tag string for limited mode.
1227
+ * Strips on* event handlers (case-insensitive) and runs sanitizeUrl()
1228
+ * on href/src/action/formaction values.
1229
+ */
1230
+ function sanitizeHtmlTagAttrs(tagStr) {
1231
+ // Self-closing or void tag without attributes — pass through
1232
+ if (!/\s/.test(tagStr.replace(/<\/?[a-zA-Z][a-zA-Z0-9]*/, '').replace(/\/?>$/, ''))) {
1233
+ return tagStr;
1234
+ }
1235
+ // Parse: <tagname ...attrs... > or <tagname ...attrs... />
1236
+ const m = tagStr.match(/^(<\/?[a-zA-Z][a-zA-Z0-9]*)([\s\S]*?)(\/?>)$/);
1237
+ /* istanbul ignore next - defensive: Phase 1.5 regex guarantees valid tag shape */
1238
+ if (!m) return tagStr;
1239
+
1240
+ const [, open, attrStr, close] = m;
1241
+ // Match individual attributes: name="value", name='value', name=value, or bare name
1242
+ // eslint-disable-next-line security/detect-unsafe-regex -- linear: no nested quantifiers
1243
+ const attrRe = /([a-zA-Z_][\w\-.:]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
1244
+ const attrs = [];
1245
+ let am;
1246
+ while ((am = attrRe.exec(attrStr)) !== null) {
1247
+ const name = am[1];
1248
+ const value = am[2] !== undefined ? am[2] : am[3] !== undefined ? am[3] : am[4];
1249
+ // Strip event handlers (on*)
1250
+ if (/^on/i.test(name)) continue;
1251
+ if (value === undefined) {
1252
+ // Boolean attribute (e.g. disabled, checked)
1253
+ attrs.push(name);
1254
+ } else {
1255
+ let sanitized = value;
1256
+ if (name.toLowerCase() in URL_ATTRIBUTES) {
1257
+ sanitized = sanitizeUrl(value);
1258
+ }
1259
+ attrs.push(`${name}="${sanitized}"`);
1260
+ }
1261
+ }
1262
+ return open + (attrs.length ? ' ' + attrs.join(' ') : '') + close;
1263
+ }
1264
+
1221
1265
  // ────────────────────────────────────────────────────────────────
1222
1266
  // Phase 1 — Code Extraction
1223
1267
  // ────────────────────────────────────────────────────────────────
@@ -1269,17 +1313,57 @@
1269
1313
  return placeholder;
1270
1314
  });
1271
1315
 
1316
+ // ────────────────────────────────────────────────────────────────
1317
+ // Phase 1.5 — Safe HTML Extraction (whitelist mode)
1318
+ // ────────────────────────────────────────────────────────────────
1319
+ // When allow_unsafe_html is an object or array, extract whitelisted
1320
+ // HTML tags, sanitize their attributes, and replace with placeholders.
1321
+ // Non-whitelisted tags stay in text so Phase 2 will escape them.
1322
+
1323
+ const safeTags = [];
1324
+ // Normalize: array → object for O(1) lookup; object used as-is
1325
+ const htmlAllow = Array.isArray(allow_unsafe_html)
1326
+ ? Object.fromEntries(allow_unsafe_html.map(t => [t, 1]))
1327
+ : (allow_unsafe_html && typeof allow_unsafe_html === 'object') ? allow_unsafe_html : null;
1328
+
1329
+ if (htmlAllow) {
1330
+ // Pass through HTML comments — browsers render them as nothing
1331
+ html = html.replace(/<!--[\s\S]*?-->/g, (match) => {
1332
+ const idx = safeTags.length;
1333
+ safeTags.push(match);
1334
+ return `${PLACEHOLDER_HT}${idx}§`;
1335
+ });
1336
+ html = html.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/g, (match, tagName) => {
1337
+ if (tagName.toLowerCase() in htmlAllow) {
1338
+ const sanitized = sanitizeHtmlTagAttrs(match);
1339
+ const idx = safeTags.length;
1340
+ safeTags.push(sanitized);
1341
+ return `${PLACEHOLDER_HT}${idx}§`;
1342
+ }
1343
+ // Not whitelisted — leave in text for Phase 2 to escape
1344
+ return match;
1345
+ });
1346
+ }
1347
+
1272
1348
  // ────────────────────────────────────────────────────────────────
1273
1349
  // Phase 2 — HTML Escaping
1274
1350
  // ────────────────────────────────────────────────────────────────
1275
1351
  // All remaining text (everything except code placeholders) is escaped
1276
1352
  // to prevent XSS. The `allow_unsafe_html` option skips this for
1277
1353
  // trusted pipelines that intentionally embed raw HTML.
1354
+ // For whitelist mode, escaping still runs (only `true` bypasses it).
1278
1355
 
1279
- if (!allow_unsafe_html) {
1356
+ if (allow_unsafe_html !== true) {
1280
1357
  html = escapeHtml(html);
1281
1358
  }
1282
1359
 
1360
+ // Restore safe HTML tag placeholders after escaping
1361
+ if (htmlAllow) {
1362
+ safeTags.forEach((tag, i) => {
1363
+ html = html.replace(`${PLACEHOLDER_HT}${i}§`, tag);
1364
+ });
1365
+ }
1366
+
1283
1367
  // ────────────────────────────────────────────────────────────────
1284
1368
  // Phase 3 — Block Scanning + Inline Formatting + Paragraphs
1285
1369
  // ────────────────────────────────────────────────────────────────
@@ -1521,6 +1605,14 @@
1521
1605
  while (i < lines.length) {
1522
1606
  const line = lines[i];
1523
1607
 
1608
+ // ── Markdown comment (reference-link hack) ──
1609
+ // [//]: # (comment) or [//]: # "comment" or [//]: #
1610
+ // These produce no output — standard markdown comment convention.
1611
+ if (/^\[\/\/\]: #/.test(line)) {
1612
+ i++;
1613
+ continue;
1614
+ }
1615
+
1524
1616
  // ── Heading ──
1525
1617
  // Count leading '#' characters. Valid heading: 1-6 hashes then a space.
1526
1618
  // Example: "## Hello World ##" → <h2>Hello World</h2>
@@ -1885,6 +1977,7 @@
1885
1977
  /** Semantic version (injected at build time) */
1886
1978
  quikdown.version = quikdownVersion;
1887
1979
 
1980
+
1888
1981
  // ════════════════════════════════════════════════════════════════════
1889
1982
  // Exports
1890
1983
  // ════════════════════════════════════════════════════════════════════