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