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