quikchat 1.1.17 → 1.2.6

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 (43) hide show
  1. package/README.md +167 -276
  2. package/dist/build-manifest.json +150 -0
  3. package/dist/quikchat-md-full.cjs.js +3023 -0
  4. package/dist/quikchat-md-full.cjs.js.map +1 -0
  5. package/dist/quikchat-md-full.cjs.min.js +10 -0
  6. package/dist/quikchat-md-full.cjs.min.js.map +1 -0
  7. package/dist/quikchat-md-full.esm.js +3021 -0
  8. package/dist/quikchat-md-full.esm.js.map +1 -0
  9. package/dist/quikchat-md-full.esm.min.js +10 -0
  10. package/dist/quikchat-md-full.esm.min.js.map +1 -0
  11. package/dist/quikchat-md-full.umd.js +3029 -0
  12. package/dist/quikchat-md-full.umd.js.map +1 -0
  13. package/dist/quikchat-md-full.umd.min.js +10 -0
  14. package/dist/quikchat-md-full.umd.min.js.map +1 -0
  15. package/dist/quikchat-md.cjs.js +1916 -0
  16. package/dist/quikchat-md.cjs.js.map +1 -0
  17. package/dist/quikchat-md.cjs.min.js +8 -0
  18. package/dist/quikchat-md.cjs.min.js.map +1 -0
  19. package/dist/quikchat-md.esm.js +1914 -0
  20. package/dist/quikchat-md.esm.js.map +1 -0
  21. package/dist/quikchat-md.esm.min.js +8 -0
  22. package/dist/quikchat-md.esm.min.js.map +1 -0
  23. package/dist/quikchat-md.umd.js +1922 -0
  24. package/dist/quikchat-md.umd.js.map +1 -0
  25. package/dist/quikchat-md.umd.min.js +8 -0
  26. package/dist/quikchat-md.umd.min.js.map +1 -0
  27. package/dist/quikchat.cjs.js +454 -1729
  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.css +1008 -250
  32. package/dist/quikchat.esm.js +454 -1729
  33. package/dist/quikchat.esm.js.map +1 -1
  34. package/dist/quikchat.esm.min.js +1 -1
  35. package/dist/quikchat.esm.min.js.map +1 -1
  36. package/dist/quikchat.min.css +1 -1
  37. package/dist/quikchat.react.js +63 -0
  38. package/dist/quikchat.umd.js +454 -1729
  39. package/dist/quikchat.umd.js.map +1 -1
  40. package/dist/quikchat.umd.min.js +1 -1
  41. package/dist/quikchat.umd.min.js.map +1 -1
  42. package/package.json +60 -39
  43. package/dist/quikchat.d.ts +0 -194
@@ -0,0 +1,1922 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3
+ typeof define === 'function' && define.amd ? define(factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.quikchat = factory());
5
+ })(this, (function () { 'use strict';
6
+
7
+ function _arrayLikeToArray(r, a) {
8
+ (null == a || a > r.length) && (a = r.length);
9
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
10
+ return n;
11
+ }
12
+ function _assertThisInitialized(e) {
13
+ if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
14
+ return e;
15
+ }
16
+ function _callSuper(t, o, e) {
17
+ return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e));
18
+ }
19
+ function _classCallCheck(a, n) {
20
+ if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function");
21
+ }
22
+ function _defineProperties(e, r) {
23
+ for (var t = 0; t < r.length; t++) {
24
+ var o = r[t];
25
+ o.enumerable = o.enumerable || false, o.configurable = true, "value" in o && (o.writable = true), Object.defineProperty(e, _toPropertyKey(o.key), o);
26
+ }
27
+ }
28
+ function _createClass(e, r, t) {
29
+ return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", {
30
+ writable: false
31
+ }), e;
32
+ }
33
+ function _createForOfIteratorHelper(r, e) {
34
+ var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
35
+ if (!t) {
36
+ if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e) {
37
+ t && (r = t);
38
+ var n = 0,
39
+ F = function () {};
40
+ return {
41
+ s: F,
42
+ n: function () {
43
+ return n >= r.length ? {
44
+ done: true
45
+ } : {
46
+ done: false,
47
+ value: r[n++]
48
+ };
49
+ },
50
+ e: function (r) {
51
+ throw r;
52
+ },
53
+ f: F
54
+ };
55
+ }
56
+ throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
57
+ }
58
+ var o,
59
+ a = true,
60
+ u = false;
61
+ return {
62
+ s: function () {
63
+ t = t.call(r);
64
+ },
65
+ n: function () {
66
+ var r = t.next();
67
+ return a = r.done, r;
68
+ },
69
+ e: function (r) {
70
+ u = true, o = r;
71
+ },
72
+ f: function () {
73
+ try {
74
+ a || null == t.return || t.return();
75
+ } finally {
76
+ if (u) throw o;
77
+ }
78
+ }
79
+ };
80
+ }
81
+ function _defineProperty(e, r, t) {
82
+ return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
83
+ value: t,
84
+ enumerable: true,
85
+ configurable: true,
86
+ writable: true
87
+ }) : e[r] = t, e;
88
+ }
89
+ function _getPrototypeOf(t) {
90
+ return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) {
91
+ return t.__proto__ || Object.getPrototypeOf(t);
92
+ }, _getPrototypeOf(t);
93
+ }
94
+ function _inherits(t, e) {
95
+ if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function");
96
+ t.prototype = Object.create(e && e.prototype, {
97
+ constructor: {
98
+ value: t,
99
+ writable: true,
100
+ configurable: true
101
+ }
102
+ }), Object.defineProperty(t, "prototype", {
103
+ writable: false
104
+ }), e && _setPrototypeOf(t, e);
105
+ }
106
+ function _isNativeReflectConstruct() {
107
+ try {
108
+ var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
109
+ } catch (t) {}
110
+ return (_isNativeReflectConstruct = function () {
111
+ return !!t;
112
+ })();
113
+ }
114
+ function ownKeys(e, r) {
115
+ var t = Object.keys(e);
116
+ if (Object.getOwnPropertySymbols) {
117
+ var o = Object.getOwnPropertySymbols(e);
118
+ r && (o = o.filter(function (r) {
119
+ return Object.getOwnPropertyDescriptor(e, r).enumerable;
120
+ })), t.push.apply(t, o);
121
+ }
122
+ return t;
123
+ }
124
+ function _objectSpread2(e) {
125
+ for (var r = 1; r < arguments.length; r++) {
126
+ var t = null != arguments[r] ? arguments[r] : {};
127
+ r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
128
+ _defineProperty(e, r, t[r]);
129
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
130
+ Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
131
+ });
132
+ }
133
+ return e;
134
+ }
135
+ function _possibleConstructorReturn(t, e) {
136
+ if (e && ("object" == typeof e || "function" == typeof e)) return e;
137
+ if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined");
138
+ return _assertThisInitialized(t);
139
+ }
140
+ function _setPrototypeOf(t, e) {
141
+ return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) {
142
+ return t.__proto__ = e, t;
143
+ }, _setPrototypeOf(t, e);
144
+ }
145
+ function _toPrimitive(t, r) {
146
+ if ("object" != typeof t || !t) return t;
147
+ var e = t[Symbol.toPrimitive];
148
+ if (void 0 !== e) {
149
+ var i = e.call(t, r);
150
+ if ("object" != typeof i) return i;
151
+ throw new TypeError("@@toPrimitive must return a primitive value.");
152
+ }
153
+ return ("string" === r ? String : Number)(t);
154
+ }
155
+ function _toPropertyKey(t) {
156
+ var i = _toPrimitive(t, "string");
157
+ return "symbol" == typeof i ? i : i + "";
158
+ }
159
+ function _unsupportedIterableToArray(r, a) {
160
+ if (r) {
161
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
162
+ var t = {}.toString.call(r).slice(8, -1);
163
+ return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
164
+ }
165
+ }
166
+
167
+ var quikchat = /*#__PURE__*/function () {
168
+ /**
169
+ *
170
+ * @param string or DOM element parentElement
171
+ * @param {*} meta
172
+ */
173
+ function quikchat(parentElement) {
174
+ var onSend = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {};
175
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
176
+ _classCallCheck(this, quikchat);
177
+ var defaultOpts = {
178
+ theme: 'quikchat-theme-light',
179
+ trackHistory: true,
180
+ showTimestamps: false,
181
+ titleArea: {
182
+ title: "Chat",
183
+ show: false,
184
+ align: "center"
185
+ },
186
+ messagesArea: {
187
+ alternating: true
188
+ }
189
+ };
190
+ var meta = _objectSpread2(_objectSpread2({}, defaultOpts), options); // merge options with defaults
191
+
192
+ if (typeof parentElement === 'string') {
193
+ parentElement = document.querySelector(parentElement);
194
+ }
195
+ this._parentElement = parentElement;
196
+ this._theme = meta.theme;
197
+ this._onSend = onSend ? onSend : function () {}; // call back function for onSend
198
+ this._messageFormatter = meta.messageFormatter || null;
199
+ this._sanitize = meta.sanitize || false;
200
+ this._createWidget();
201
+ // title area
202
+ if (meta.titleArea) {
203
+ this.titleAreaSetContents(meta.titleArea.title, meta.titleArea.align);
204
+ if (meta.titleArea.show === true) {
205
+ this.titleAreaShow();
206
+ } else {
207
+ this.titleAreaHide();
208
+ }
209
+ }
210
+ // messages area
211
+ if (meta.messagesArea) {
212
+ this.messagesAreaAlternateColors(meta.messagesArea.alternating);
213
+ }
214
+ // timestamps
215
+ if (meta.showTimestamps) {
216
+ this.messagesAreaShowTimestamps(true);
217
+ }
218
+ // direction (ltr/rtl)
219
+ if (meta.direction) {
220
+ this.setDirection(meta.direction);
221
+ }
222
+ // plumbing
223
+ this._attachEventListeners();
224
+ this.trackHistory = meta.trackHistory !== false;
225
+ this._historyLimit = 10000000;
226
+ this._history = [];
227
+ }
228
+ return _createClass(quikchat, [{
229
+ key: "_createWidget",
230
+ value: function _createWidget() {
231
+ var widgetHTML = "\n <div class=\"quikchat-base ".concat(this.theme, "\">\n <div class=\"quikchat-title-area\"></div>\n <div class=\"quikchat-messages-wrapper\"><div class=\"quikchat-messages-area\" role=\"log\" aria-live=\"polite\" aria-label=\"Chat messages\"></div><button class=\"quikchat-scroll-bottom\" aria-label=\"Scroll to bottom\"></button></div>\n <div class=\"quikchat-input-area\">\n <textarea class=\"quikchat-input-textbox\" rows=\"1\" aria-label=\"Type a message\"></textarea>\n <button class=\"quikchat-input-send-btn\">Send</button>\n </div>\n </div>\n ");
232
+ this._parentElement.innerHTML = widgetHTML;
233
+ this._chatWidget = this._parentElement.querySelector('.quikchat-base');
234
+ this._titleArea = this._chatWidget.querySelector('.quikchat-title-area');
235
+ this._messagesWrapper = this._chatWidget.querySelector('.quikchat-messages-wrapper');
236
+ this._messagesArea = this._chatWidget.querySelector('.quikchat-messages-area');
237
+ this._scrollBottomBtn = this._messagesWrapper.querySelector('.quikchat-scroll-bottom');
238
+ this._inputArea = this._chatWidget.querySelector('.quikchat-input-area');
239
+ this._textEntry = this._inputArea.querySelector('.quikchat-input-textbox');
240
+ this._sendButton = this._inputArea.querySelector('.quikchat-input-send-btn');
241
+ this.msgid = 0;
242
+ }
243
+
244
+ /**
245
+ * Attach event listeners to the widget
246
+ */
247
+ }, {
248
+ key: "_attachEventListeners",
249
+ value: function _attachEventListeners() {
250
+ var _this = this;
251
+ this._sendButton.addEventListener('click', function () {
252
+ var text = _this._textEntry.value.trim();
253
+ if (text === '') return;
254
+ _this._onSend(_this, text);
255
+ });
256
+ this._textEntry.addEventListener('keydown', function (event) {
257
+ // Check if Shift + Enter is pressed
258
+ if (event.shiftKey && event.keyCode === 13) {
259
+ event.preventDefault();
260
+ var text = _this._textEntry.value.trim();
261
+ if (text === '') return;
262
+ _this._onSend(_this, text);
263
+ }
264
+ });
265
+
266
+ // Auto-grow textarea
267
+ this._textEntry.addEventListener('input', function () {
268
+ return _this._autoGrowTextarea();
269
+ });
270
+ this._messagesArea.addEventListener('scroll', function () {
271
+ var _this$_messagesArea = _this._messagesArea,
272
+ scrollTop = _this$_messagesArea.scrollTop,
273
+ scrollHeight = _this$_messagesArea.scrollHeight,
274
+ clientHeight = _this$_messagesArea.clientHeight;
275
+ _this.userScrolledUp = scrollTop + clientHeight < scrollHeight - 1;
276
+ _this._updateScrollBottomBtn();
277
+ });
278
+
279
+ // Scroll-to-bottom button
280
+ this._scrollBottomBtn.addEventListener('click', function () {
281
+ return _this.scrollToBottom();
282
+ });
283
+
284
+ // Ctrl+End to scroll to bottom
285
+ this._chatWidget.addEventListener('keydown', function (event) {
286
+ if (event.ctrlKey && event.key === 'End') {
287
+ event.preventDefault();
288
+ _this.scrollToBottom();
289
+ }
290
+ });
291
+
292
+ // Use ResizeObserver to detect parent container resize
293
+ if (typeof ResizeObserver !== 'undefined') {
294
+ this._resizeObserver = new ResizeObserver(function () {
295
+ return _this._handleContainerResize();
296
+ });
297
+ this._resizeObserver.observe(this._parentElement);
298
+ }
299
+ }
300
+
301
+ // set the onSend function callback.
302
+ }, {
303
+ key: "setCallbackOnSend",
304
+ value: function setCallbackOnSend(callback) {
305
+ this._onSend = callback;
306
+ }
307
+ // set a callback for everytime a message is added (listener)
308
+ }, {
309
+ key: "setCallbackonMessageAdded",
310
+ value: function setCallbackonMessageAdded(callback) {
311
+ this._onMessageAdded = callback;
312
+ }
313
+ }, {
314
+ key: "setCallbackonMessageAppend",
315
+ value: function setCallbackonMessageAppend(callback) {
316
+ this._onMessageAppend = callback;
317
+ }
318
+ }, {
319
+ key: "setCallbackonMessageReplace",
320
+ value: function setCallbackonMessageReplace(callback) {
321
+ this._onMessageReplace = callback;
322
+ }
323
+ }, {
324
+ key: "setCallbackonMessageDelete",
325
+ value: function setCallbackonMessageDelete(callback) {
326
+ this._onMessageDelete = callback;
327
+ }
328
+
329
+ // Public methods
330
+ }, {
331
+ key: "titleAreaToggle",
332
+ value: function titleAreaToggle() {
333
+ if (this._titleArea.style.display === 'none') {
334
+ this.titleAreaShow();
335
+ } else {
336
+ this.titleAreaHide();
337
+ }
338
+ }
339
+ }, {
340
+ key: "titleAreaShow",
341
+ value: function titleAreaShow() {
342
+ this._titleArea.style.display = '';
343
+ }
344
+ }, {
345
+ key: "titleAreaHide",
346
+ value: function titleAreaHide() {
347
+ this._titleArea.style.display = 'none';
348
+ }
349
+ }, {
350
+ key: "titleAreaSetContents",
351
+ value: function titleAreaSetContents(title) {
352
+ var align = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'center';
353
+ this._titleArea.innerHTML = title;
354
+ this._titleArea.style.textAlign = align;
355
+ }
356
+ }, {
357
+ key: "titleAreaGetContents",
358
+ value: function titleAreaGetContents() {
359
+ return this._titleArea.innerHTML;
360
+ }
361
+ }, {
362
+ key: "inputAreaToggle",
363
+ value: function inputAreaToggle() {
364
+ if (this._inputArea.style.display === 'none') {
365
+ this.inputAreaShow();
366
+ } else {
367
+ this.inputAreaHide();
368
+ }
369
+ }
370
+ }, {
371
+ key: "inputAreaShow",
372
+ value: function inputAreaShow() {
373
+ this._inputArea.style.display = '';
374
+ }
375
+ }, {
376
+ key: "inputAreaHide",
377
+ value: function inputAreaHide() {
378
+ this._inputArea.style.display = 'none';
379
+ }
380
+ }, {
381
+ key: "inputAreaSetEnabled",
382
+ value: function inputAreaSetEnabled(enabled) {
383
+ this._textEntry.disabled = !enabled;
384
+ this._sendButton.disabled = !enabled;
385
+ }
386
+ }, {
387
+ key: "inputAreaSetButtonText",
388
+ value: function inputAreaSetButtonText(text) {
389
+ this._sendButton.textContent = text;
390
+ }
391
+ }, {
392
+ key: "inputAreaGetButtonText",
393
+ value: function inputAreaGetButtonText() {
394
+ return this._sendButton.textContent;
395
+ }
396
+ }, {
397
+ key: "setDirection",
398
+ value: function setDirection(dir) {
399
+ var d = dir === 'rtl' ? 'rtl' : 'ltr';
400
+ this._chatWidget.setAttribute('dir', d);
401
+ if (d === 'rtl') {
402
+ this._chatWidget.classList.add('quikchat-rtl');
403
+ } else {
404
+ this._chatWidget.classList.remove('quikchat-rtl');
405
+ }
406
+ }
407
+ }, {
408
+ key: "getDirection",
409
+ value: function getDirection() {
410
+ return this._chatWidget.getAttribute('dir') || 'ltr';
411
+ }
412
+ }, {
413
+ key: "_handleContainerResize",
414
+ value: function _handleContainerResize() {
415
+ // Layout is handled by CSS flexbox — no JS height calculation needed.
416
+ // This hook exists for future use or custom resize callbacks.
417
+ }
418
+ }, {
419
+ key: "scrollToBottom",
420
+ value: function scrollToBottom() {
421
+ this._messagesArea.scrollTop = this._messagesArea.scrollHeight;
422
+ this.userScrolledUp = false;
423
+ this._updateScrollBottomBtn();
424
+ }
425
+ }, {
426
+ key: "_updateScrollBottomBtn",
427
+ value: function _updateScrollBottomBtn() {
428
+ if (this.userScrolledUp) {
429
+ this._scrollBottomBtn.classList.add('quikchat-scroll-bottom-visible');
430
+ } else {
431
+ this._scrollBottomBtn.classList.remove('quikchat-scroll-bottom-visible');
432
+ }
433
+ }
434
+ }, {
435
+ key: "_autoGrowTextarea",
436
+ value: function _autoGrowTextarea() {
437
+ var el = this._textEntry;
438
+ el.style.height = 'auto';
439
+ var maxHeight = 120;
440
+ el.style.height = Math.min(el.scrollHeight, maxHeight) + 'px';
441
+ el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';
442
+ }
443
+ }, {
444
+ key: "_formatTimestamp",
445
+ value: function _formatTimestamp(isoString) {
446
+ var d = new Date(isoString);
447
+ var h = d.getHours();
448
+ var m = String(d.getMinutes()).padStart(2, '0');
449
+ var ampm = h >= 12 ? 'PM' : 'AM';
450
+ var h12 = h % 12 || 12;
451
+ return h12 + ':' + m + ' ' + ampm;
452
+ }
453
+ }, {
454
+ key: "messagesAreaShowTimestamps",
455
+ value: function messagesAreaShowTimestamps(show) {
456
+ if (show) {
457
+ this._messagesArea.classList.add('quikchat-show-timestamps');
458
+ } else {
459
+ this._messagesArea.classList.remove('quikchat-show-timestamps');
460
+ }
461
+ }
462
+ }, {
463
+ key: "messagesAreaShowTimestampsGet",
464
+ value: function messagesAreaShowTimestampsGet() {
465
+ return this._messagesArea.classList.contains('quikchat-show-timestamps');
466
+ }
467
+ }, {
468
+ key: "messagesAreaShowTimestampsToggle",
469
+ value: function messagesAreaShowTimestampsToggle() {
470
+ this._messagesArea.classList.toggle('quikchat-show-timestamps');
471
+ }
472
+ }, {
473
+ key: "_escapeHTML",
474
+ value: function _escapeHTML(str) {
475
+ var div = document.createElement('div');
476
+ div.textContent = str;
477
+ return div.innerHTML;
478
+ }
479
+ }, {
480
+ key: "_processContent",
481
+ value: function _processContent(content) {
482
+ if (this._sanitize === true) {
483
+ content = this._escapeHTML(content);
484
+ } else if (typeof this._sanitize === 'function') {
485
+ content = this._sanitize(content);
486
+ }
487
+ if (this._messageFormatter) {
488
+ content = this._messageFormatter(content);
489
+ }
490
+ return content;
491
+ }
492
+
493
+ //messagesArea functions
494
+ }, {
495
+ key: "messagesAreaAlternateColors",
496
+ value: function messagesAreaAlternateColors() {
497
+ var alt = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
498
+ if (alt) {
499
+ this._messagesArea.classList.add('quikchat-messages-area-alt');
500
+ } else {
501
+ this._messagesArea.classList.remove('quikchat-messages-area-alt');
502
+ }
503
+ return alt === true;
504
+ }
505
+ }, {
506
+ key: "messagesAreaAlternateColorsToggle",
507
+ value: function messagesAreaAlternateColorsToggle() {
508
+ this._messagesArea.classList.toggle('quikchat-messages-area-alt');
509
+ }
510
+ }, {
511
+ key: "messagesAreaAlternateColorsGet",
512
+ value: function messagesAreaAlternateColorsGet() {
513
+ return this._messagesArea.classList.contains('quikchat-messages-area-alt');
514
+ }
515
+ // message functions
516
+ }, {
517
+ key: "messageAddFull",
518
+ value: function messageAddFull() {
519
+ var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
520
+ content: "",
521
+ userString: "user",
522
+ align: "right",
523
+ role: "user",
524
+ userID: -1
525
+ };
526
+ var msgid = this.msgid;
527
+ var messageDiv = document.createElement('div');
528
+ var msgidClass = 'quikchat-msgid-' + String(msgid).padStart(10, '0');
529
+ messageDiv.classList.add('quikchat-message', msgidClass);
530
+ messageDiv.classList.add('quikchat-role-' + (input.role || 'user'));
531
+ messageDiv.classList.add('quikchat-align-' + (input.align || 'right'));
532
+ this.msgid++;
533
+ messageDiv.classList.add(this._messagesArea.children.length % 2 === 1 ? 'quikchat-message-1' : 'quikchat-message-2');
534
+
535
+ // Visibility: default true, hidden messages get display:none
536
+ var visible = input.visible !== false;
537
+ if (!visible) {
538
+ messageDiv.style.display = 'none';
539
+ }
540
+
541
+ // Tags: array of strings for group-based visibility control
542
+ var tags = Array.isArray(input.tags) ? input.tags.slice() : [];
543
+ var userDiv = document.createElement('div');
544
+ userDiv.classList.add('quikchat-user-label');
545
+ userDiv.style.textAlign = input.align;
546
+ userDiv.innerHTML = input.userString;
547
+ var contentDiv = document.createElement('div');
548
+ contentDiv.classList.add('quikchat-message-content');
549
+ contentDiv.style.textAlign = input.align;
550
+ contentDiv.innerHTML = this._processContent(input.content);
551
+ var timestamp = new Date().toISOString();
552
+ var timestampSpan = document.createElement('span');
553
+ timestampSpan.classList.add('quikchat-timestamp');
554
+ timestampSpan.textContent = this._formatTimestamp(timestamp);
555
+ messageDiv.appendChild(userDiv);
556
+ messageDiv.appendChild(contentDiv);
557
+ messageDiv.appendChild(timestampSpan);
558
+ this._messagesArea.appendChild(messageDiv);
559
+
560
+ // Scroll to the last message only if the user is not actively scrolling up
561
+ if (!this.userScrolledUp) {
562
+ this._messagesArea.scrollTop = this._messagesArea.scrollHeight;
563
+ }
564
+ this._textEntry.value = '';
565
+ this._autoGrowTextarea();
566
+ var updatedtime = timestamp;
567
+ if (this.trackHistory) {
568
+ this._history.push(_objectSpread2(_objectSpread2({
569
+ msgid: msgid
570
+ }, input), {}, {
571
+ visible: visible,
572
+ tags: tags,
573
+ timestamp: timestamp,
574
+ updatedtime: updatedtime,
575
+ messageDiv: messageDiv
576
+ }));
577
+ if (this._history.length > this._historyLimit) {
578
+ this._history.shift();
579
+ }
580
+ }
581
+ if (this._onMessageAdded) {
582
+ this._onMessageAdded(this, msgid);
583
+ }
584
+ return msgid;
585
+ }
586
+ }, {
587
+ key: "messageAddNew",
588
+ value: function messageAddNew() {
589
+ var content = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
590
+ var userString = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "user";
591
+ var align = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "right";
592
+ var role = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "user";
593
+ return this.messageAddFull({
594
+ content: content,
595
+ userString: userString,
596
+ align: align,
597
+ role: role
598
+ });
599
+ }
600
+ }, {
601
+ key: "messageRemove",
602
+ value: function messageRemove(n) {
603
+ var success = false;
604
+ try {
605
+ this._messagesArea.removeChild(this._messagesArea.querySelector(".quikchat-msgid-".concat(String(n).padStart(10, '0'))));
606
+ success = true;
607
+ } catch (_error) {
608
+ // Message ID not found
609
+ }
610
+ if (success) {
611
+ this._history.splice(this._history.findIndex(function (item) {
612
+ return item.msgid === n;
613
+ }), 1);
614
+ if (this._onMessageDelete) {
615
+ this._onMessageDelete(this, n);
616
+ }
617
+ }
618
+ return success;
619
+ }
620
+ /* returns the message html object from the DOM
621
+ */
622
+ }, {
623
+ key: "messageGetDOMObject",
624
+ value: function messageGetDOMObject(n) {
625
+ var msg = null;
626
+ try {
627
+ msg = this._messagesArea.querySelector(".quikchat-msgid-".concat(String(n).padStart(10, '0')));
628
+ } catch (_error) {
629
+ // Message ID not found
630
+ }
631
+ return msg;
632
+ }
633
+ /* returns the message content only
634
+ */
635
+ }, {
636
+ key: "messageGetContent",
637
+ value: function messageGetContent(n) {
638
+ var content = "";
639
+ try {
640
+ content = this._history.filter(function (item) {
641
+ return item.msgid === n;
642
+ })[0].content;
643
+ } catch (_error) {
644
+ // Message ID not found
645
+ }
646
+ return content;
647
+ }
648
+ }, {
649
+ key: "messageSetVisible",
650
+ value: function messageSetVisible(n, visible) {
651
+ var msgEl = this.messageGetDOMObject(n);
652
+ if (!msgEl) return false;
653
+ msgEl.style.display = visible ? '' : 'none';
654
+ var item = this._history.find(function (entry) {
655
+ return entry.msgid === n;
656
+ });
657
+ if (item) item.visible = visible;
658
+ return true;
659
+ }
660
+ }, {
661
+ key: "messageGetVisible",
662
+ value: function messageGetVisible(n) {
663
+ var item = this._history.find(function (entry) {
664
+ return entry.msgid === n;
665
+ });
666
+ return item ? item.visible !== false : false;
667
+ }
668
+ }, {
669
+ key: "messageToggleVisible",
670
+ value: function messageToggleVisible(n) {
671
+ var current = this.messageGetVisible(n);
672
+ return this.messageSetVisible(n, !current);
673
+ }
674
+ }, {
675
+ key: "messageSetVisibleByTag",
676
+ value: function messageSetVisibleByTag(tag, visible) {
677
+ var count = 0;
678
+ var _iterator = _createForOfIteratorHelper(this._history),
679
+ _step;
680
+ try {
681
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
682
+ var item = _step.value;
683
+ if (item.tags && item.tags.includes(tag)) {
684
+ this.messageSetVisible(item.msgid, visible);
685
+ count++;
686
+ }
687
+ }
688
+ } catch (err) {
689
+ _iterator.e(err);
690
+ } finally {
691
+ _iterator.f();
692
+ }
693
+ return count;
694
+ }
695
+ }, {
696
+ key: "messageGetTags",
697
+ value: function messageGetTags(n) {
698
+ var item = this._history.find(function (entry) {
699
+ return entry.msgid === n;
700
+ });
701
+ return item && item.tags ? item.tags.slice() : [];
702
+ }
703
+ }, {
704
+ key: "messageSetTags",
705
+ value: function messageSetTags(n, tags) {
706
+ var item = this._history.find(function (entry) {
707
+ return entry.msgid === n;
708
+ });
709
+ if (!item) return false;
710
+ item.tags = Array.isArray(tags) ? tags.slice() : [];
711
+ return true;
712
+ }
713
+
714
+ /* append message to the message content
715
+ */
716
+ }, {
717
+ key: "messageAppendContent",
718
+ value: function messageAppendContent(n, content) {
719
+ var success = false;
720
+ try {
721
+ var msgEl = this._messagesArea.querySelector(".quikchat-msgid-".concat(String(n).padStart(10, '0')));
722
+ var item = this._history.filter(function (entry) {
723
+ return entry.msgid === n;
724
+ })[0];
725
+ item.content += content;
726
+ item.updatedtime = new Date().toISOString();
727
+ msgEl.querySelector('.quikchat-message-content').innerHTML = this._processContent(item.content);
728
+ msgEl.classList.remove('quikchat-typing');
729
+ success = true;
730
+ if (!this.userScrolledUp) {
731
+ this._messagesArea.scrollTop = this._messagesArea.scrollHeight;
732
+ }
733
+ if (this._onMessageAppend) {
734
+ this._onMessageAppend(this, n, content);
735
+ }
736
+ } catch (_error) {
737
+ // Message ID not found
738
+ }
739
+ return success;
740
+ }
741
+
742
+ /* replace message content
743
+ */
744
+ }, {
745
+ key: "messageReplaceContent",
746
+ value: function messageReplaceContent(n, content) {
747
+ var success = false;
748
+ try {
749
+ var msgEl = this._messagesArea.querySelector(".quikchat-msgid-".concat(String(n).padStart(10, '0')));
750
+ var item = this._history.filter(function (entry) {
751
+ return entry.msgid === n;
752
+ })[0];
753
+ item.content = content;
754
+ item.updatedtime = new Date().toISOString();
755
+ msgEl.querySelector('.quikchat-message-content').innerHTML = this._processContent(content);
756
+ msgEl.classList.remove('quikchat-typing');
757
+ success = true;
758
+ if (!this.userScrolledUp) {
759
+ this._messagesArea.scrollTop = this._messagesArea.scrollHeight;
760
+ }
761
+ if (this._onMessageReplace) {
762
+ this._onMessageReplace(this, n, content);
763
+ }
764
+ } catch (_error) {
765
+ // Message ID not found
766
+ }
767
+ return success;
768
+ }
769
+ }, {
770
+ key: "messageAddTypingIndicator",
771
+ value: function messageAddTypingIndicator() {
772
+ var userString = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
773
+ var align = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'left';
774
+ var msgid = this.messageAddFull({
775
+ content: '',
776
+ userString: userString,
777
+ align: align,
778
+ role: 'assistant'
779
+ });
780
+ var msgEl = this.messageGetDOMObject(msgid);
781
+ msgEl.classList.add('quikchat-typing');
782
+ var contentDiv = msgEl.querySelector('.quikchat-message-content');
783
+ contentDiv.innerHTML = '<span class="quikchat-typing-dots"><span>.</span><span>.</span><span>.</span></span>';
784
+ return msgid;
785
+ }
786
+ }, {
787
+ key: "setMessageFormatter",
788
+ value: function setMessageFormatter(formatter) {
789
+ this._messageFormatter = formatter;
790
+ }
791
+ }, {
792
+ key: "setSanitize",
793
+ value: function setSanitize(sanitize) {
794
+ this._sanitize = sanitize;
795
+ }
796
+
797
+ // history functions
798
+ /**
799
+ *
800
+ * @param {*} n
801
+ * @param {*} m
802
+ * @returns array of history messages
803
+ */
804
+ }, {
805
+ key: "historyGet",
806
+ value: function historyGet(n, m) {
807
+ if (n === undefined) {
808
+ return this._history.slice();
809
+ }
810
+ if (m === undefined) {
811
+ if (n < 0) {
812
+ return this._history.slice(n);
813
+ }
814
+ return this._history.slice(n, n + 1);
815
+ }
816
+ return this._history.slice(n, m);
817
+ }
818
+ }, {
819
+ key: "historyClear",
820
+ value: function historyClear() {
821
+ this.msgid = 0;
822
+ this._history = [];
823
+ }
824
+ }, {
825
+ key: "historyGetLength",
826
+ value: function historyGetLength() {
827
+ return this._history.length;
828
+ }
829
+ }, {
830
+ key: "historyGetMessage",
831
+ value: function historyGetMessage(n) {
832
+ if (n >= 0 && n < this._history.length) {
833
+ return this._history[n];
834
+ }
835
+ return {};
836
+ }
837
+ }, {
838
+ key: "historyGetMessageContent",
839
+ value: function historyGetMessageContent(n) {
840
+ if (n >= 0 && n < this._history.length) {
841
+ return this._history[n].content;
842
+ }
843
+ return "";
844
+ }
845
+ }, {
846
+ key: "historyExport",
847
+ value: function historyExport() {
848
+ return this._history.map(function (item) {
849
+ return {
850
+ msgid: item.msgid,
851
+ content: item.content,
852
+ userString: item.userString,
853
+ align: item.align,
854
+ role: item.role,
855
+ userID: item.userID,
856
+ visible: item.visible,
857
+ tags: item.tags ? item.tags.slice() : [],
858
+ timestamp: item.timestamp,
859
+ updatedtime: item.updatedtime
860
+ };
861
+ });
862
+ }
863
+ }, {
864
+ key: "historyImport",
865
+ value: function historyImport(data) {
866
+ // Clear existing messages from DOM and history
867
+ this._messagesArea.innerHTML = '';
868
+ this._history = [];
869
+ this.msgid = 0;
870
+ var _iterator2 = _createForOfIteratorHelper(data),
871
+ _step2;
872
+ try {
873
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
874
+ var entry = _step2.value;
875
+ this.messageAddFull({
876
+ content: entry.content || '',
877
+ userString: entry.userString || 'user',
878
+ align: entry.align || 'right',
879
+ role: entry.role || 'user',
880
+ userID: entry.userID,
881
+ visible: entry.visible,
882
+ tags: entry.tags
883
+ });
884
+ }
885
+ } catch (err) {
886
+ _iterator2.e(err);
887
+ } finally {
888
+ _iterator2.f();
889
+ }
890
+ }
891
+ }, {
892
+ key: "changeTheme",
893
+ value: function changeTheme(newTheme) {
894
+ this._chatWidget.classList.remove(this._theme);
895
+ this._chatWidget.classList.add(newTheme);
896
+ this._theme = newTheme;
897
+ }
898
+ }, {
899
+ key: "theme",
900
+ get: function get() {
901
+ return this._theme;
902
+ }
903
+ }], [{
904
+ key: "version",
905
+ value: function version() {
906
+ return {
907
+ "version": "1.2.6",
908
+ "license": "BSD-2",
909
+ "url": "https://github/deftio/quikchat"
910
+ };
911
+ }
912
+
913
+ /**
914
+ * quikchat.loremIpsum() - Generate a simple string of Lorem Ipsum text (sample typographer's text) of numChars in length.
915
+ * borrowed from github.com/deftio/bitwrench.js
916
+ * @param {number} numChars - The number of characters to generate (random btw 25 and 150 if undefined).
917
+ * @param {number} [startSpot=0] - The starting index in the Lorem Ipsum text. If undefined, a random startSpot will be generated.
918
+ * @param {boolean} [startWithCapitalLetter=true] - If true, capitalize the first character or inject a capital letter if the first character isn't a capital letter.
919
+ *
920
+ * @returns {string} A string of Lorem Ipsum text.
921
+ *
922
+ * @example
923
+ * // Returns 200 characters of Lorem Ipsum starting from index 50
924
+ * loremIpsum(200, 50);
925
+ *
926
+ * @example
927
+ * //Returns a 200 Lorem Ipsum characters starting from a random index
928
+ * loremIpsum(200);
929
+ */
930
+ }, {
931
+ key: "loremIpsum",
932
+ value: function loremIpsum(numChars) {
933
+ var startSpot = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
934
+ var startWithCapitalLetter = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
935
+ var loremText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ";
936
+ if (typeof numChars !== "number") {
937
+ numChars = Math.floor(Math.random() * 126) + 25;
938
+ }
939
+ if (startSpot === undefined) {
940
+ startSpot = Math.floor(Math.random() * loremText.length);
941
+ }
942
+ startSpot = startSpot % loremText.length;
943
+
944
+ // Move startSpot to the next non-whitespace and non-punctuation character
945
+ while (loremText[startSpot] === ' ' || /[.,:;!?]/.test(loremText[startSpot])) {
946
+ startSpot = (startSpot + 1) % loremText.length;
947
+ }
948
+ var l = loremText.substring(startSpot) + loremText.substring(0, startSpot);
949
+ var s = "";
950
+ while (numChars > 0) {
951
+ s += numChars < l.length ? l.substring(0, numChars) : l;
952
+ numChars -= l.length;
953
+ }
954
+ if (s[s.length - 1] === " ") {
955
+ s = s.substring(0, s.length - 1) + "."; // always end on non-whitespace. "." was chosen arbitrarily.
956
+ }
957
+ if (startWithCapitalLetter) {
958
+ s = s[0].toUpperCase() + s.substring(1);
959
+ }
960
+ return s;
961
+ }
962
+ }]);
963
+ }();
964
+
965
+ /**
966
+ * quikdown - Lightweight Markdown Parser
967
+ * @version 1.2.10
968
+ * @license BSD-2-Clause
969
+ * @copyright DeftIO 2025
970
+ */
971
+ /**
972
+ * quikdown_classify — Shared line-classification utilities
973
+ * ═════════════════════════════════════════════════════════
974
+ *
975
+ * Pure functions for classifying markdown lines. Used by both the main
976
+ * parser (quikdown.js) and the editor (quikdown_edit.js) so the logic
977
+ * lives in one place.
978
+ *
979
+ * All functions operate on a **trimmed** line (caller must trim).
980
+ * None use regexes with nested quantifiers — every check is either a
981
+ * simple regex or a linear scan, so there is zero ReDoS risk.
982
+ */
983
+
984
+
985
+ /**
986
+ * Dash-only HR check — exact parity with the main parser's original
987
+ * regex `/^---+\s*$/`. Only matches lines of three or more dashes
988
+ * with optional trailing whitespace (no interspersed spaces).
989
+ *
990
+ * @param {string} trimmed The line, already trimmed
991
+ * @returns {boolean}
992
+ */
993
+ function isDashHRLine(trimmed) {
994
+ if (trimmed.length < 3) return false;
995
+ for (let i = 0; i < trimmed.length; i++) {
996
+ const ch = trimmed[i];
997
+ if (ch === '-') continue;
998
+ // Allow trailing whitespace only
999
+ if (ch === ' ' || ch === '\t') {
1000
+ for (let j = i + 1; j < trimmed.length; j++) {
1001
+ if (trimmed[j] !== ' ' && trimmed[j] !== '\t') return false;
1002
+ }
1003
+ return i >= 3; // at least 3 dashes before whitespace
1004
+ }
1005
+ return false;
1006
+ }
1007
+ return true; // all dashes
1008
+ }
1009
+
1010
+ /**
1011
+ * quikdown — A compact, scanner-based markdown parser
1012
+ * ════════════════════════════════════════════════════
1013
+ *
1014
+ * Architecture overview (v1.2.8 — lexer rewrite)
1015
+ * ───────────────────────────────────────────────
1016
+ * Prior to v1.2.8, quikdown used a multi-pass regex pipeline: each block
1017
+ * type (headings, blockquotes, HR, lists, tables) and each inline format
1018
+ * (bold, italic, links, …) was handled by its own global regex applied
1019
+ * sequentially to the full document string. That worked but made the code
1020
+ * hard to extend and debug — a new construct meant adding another regex
1021
+ * pass, and ordering bugs between passes were subtle.
1022
+ *
1023
+ * Starting in v1.2.8 the parser uses a **line-scanning** approach for
1024
+ * block detection and a **per-block inline pass** for formatting:
1025
+ *
1026
+ * ┌─────────────────────────────────────────────────────────┐
1027
+ * │ Phase 1 — Code Extraction │
1028
+ * │ Scan for fenced code blocks (``` / ~~~) and inline │
1029
+ * │ code spans (`…`). Replace with §CB§ / §IC§ place- │
1030
+ * │ holders so code content is never touched by later │
1031
+ * │ phases. │
1032
+ * ├─────────────────────────────────────────────────────────┤
1033
+ * │ Phase 2 — HTML Escaping │
1034
+ * │ Escape &, <, >, ", ' in the remaining text to prevent │
1035
+ * │ XSS. (Skipped when allow_unsafe_html is true.) │
1036
+ * ├─────────────────────────────────────────────────────────┤
1037
+ * │ Phase 3 — Block Scanning │
1038
+ * │ Walk the text **line by line**. At each line, the │
1039
+ * │ scanner checks (in order): │
1040
+ * │ • table rows (|) │
1041
+ * │ • headings (#) │
1042
+ * │ • HR (---) │
1043
+ * │ • blockquotes (&gt;) │
1044
+ * │ • list items (-, *, +, 1.) │
1045
+ * │ • code-block placeholder (§CB…§) │
1046
+ * │ • paragraph text (everything else) │
1047
+ * │ │
1048
+ * │ Block text is run through the **inline formatter** │
1049
+ * │ which handles bold, italic, strikethrough, links, │
1050
+ * │ images, and autolinks. │
1051
+ * │ │
1052
+ * │ Paragraphs are wrapped in <p> tags. Lazy linefeeds │
1053
+ * │ (single \n → <br>) are handled here too. │
1054
+ * ├─────────────────────────────────────────────────────────┤
1055
+ * │ Phase 4 — Code Restoration │
1056
+ * │ Replace §CB§ / §IC§ placeholders with rendered <pre> │
1057
+ * │ / <code> HTML, applying the fence_plugin if present. │
1058
+ * └─────────────────────────────────────────────────────────┘
1059
+ *
1060
+ * Why this design?
1061
+ * • Single pass over lines for block identification — no re-scanning.
1062
+ * • Each block type is a clearly separated branch, easy to add new ones.
1063
+ * • Inline formatting is confined to block text — can't accidentally
1064
+ * match across block boundaries or inside HTML tags.
1065
+ * • Code extraction still uses a simple regex (it's one pattern, not a
1066
+ * chain) because the §-placeholder approach is proven and simple.
1067
+ *
1068
+ * @param {string} markdown The markdown source text
1069
+ * @param {Object} options Configuration (see below)
1070
+ * @returns {string} Rendered HTML
1071
+ */
1072
+
1073
+
1074
+ // ────────────────────────────────────────────────────────────────────
1075
+ // Constants
1076
+ // ────────────────────────────────────────────────────────────────────
1077
+
1078
+ /** Build-time version stamp (injected by tools/updateVersion) */
1079
+ const quikdownVersion = '1.2.10';
1080
+
1081
+ /** CSS class prefix used for all generated elements */
1082
+ const CLASS_PREFIX = 'quikdown-';
1083
+
1084
+ /** Placeholder sigils — chosen to be extremely unlikely in real text */
1085
+ const PLACEHOLDER_CB = '§CB'; // fenced code blocks
1086
+ const PLACEHOLDER_IC = '§IC'; // inline code spans
1087
+
1088
+ /** HTML entity escape map */
1089
+ const ESC_MAP = {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'};
1090
+
1091
+ // ────────────────────────────────────────────────────────────────────
1092
+ // Style definitions
1093
+ // ────────────────────────────────────────────────────────────────────
1094
+
1095
+ /**
1096
+ * Inline styles for every element quikdown can emit.
1097
+ * When `inline_styles: true` these are injected as style="…" attributes.
1098
+ * When `inline_styles: false` (default) we use class="quikdown-<tag>"
1099
+ * and these same values are emitted by `quikdown.emitStyles()`.
1100
+ */
1101
+ const QUIKDOWN_STYLES = {
1102
+ h1: 'font-size:2em;font-weight:600;margin:.67em 0;text-align:left',
1103
+ h2: 'font-size:1.5em;font-weight:600;margin:.83em 0',
1104
+ h3: 'font-size:1.25em;font-weight:600;margin:1em 0',
1105
+ h4: 'font-size:1em;font-weight:600;margin:1.33em 0',
1106
+ h5: 'font-size:.875em;font-weight:600;margin:1.67em 0',
1107
+ h6: 'font-size:.85em;font-weight:600;margin:2em 0',
1108
+ pre: 'background:#f4f4f4;padding:10px;border-radius:4px;overflow-x:auto;margin:1em 0',
1109
+ code: 'background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace',
1110
+ blockquote: 'border-left:4px solid #ddd;margin-left:0;padding-left:1em',
1111
+ table: 'border-collapse:collapse;width:100%;margin:1em 0',
1112
+ th: 'border:1px solid #ddd;padding:8px;background-color:#f2f2f2;font-weight:bold;text-align:left',
1113
+ td: 'border:1px solid #ddd;padding:8px;text-align:left',
1114
+ hr: 'border:none;border-top:1px solid #ddd;margin:1em 0',
1115
+ img: 'max-width:100%;height:auto',
1116
+ a: 'color:#06c;text-decoration:underline',
1117
+ strong: 'font-weight:bold',
1118
+ em: 'font-style:italic',
1119
+ del: 'text-decoration:line-through',
1120
+ ul: 'margin:.5em 0;padding-left:2em',
1121
+ ol: 'margin:.5em 0;padding-left:2em',
1122
+ li: 'margin:.25em 0',
1123
+ 'task-item': 'list-style:none',
1124
+ 'task-checkbox': 'margin-right:.5em'
1125
+ };
1126
+
1127
+ // ────────────────────────────────────────────────────────────────────
1128
+ // Attribute factory
1129
+ // ────────────────────────────────────────────────────────────────────
1130
+
1131
+ /**
1132
+ * Creates a `getAttr(tag, additionalStyle?)` helper that returns
1133
+ * either a class="…" or style="…" attribute string depending on mode.
1134
+ *
1135
+ * @param {boolean} inline_styles True → emit style="…"; false → class="…"
1136
+ * @param {Object} styles The QUIKDOWN_STYLES map
1137
+ * @returns {Function}
1138
+ */
1139
+ function createGetAttr(inline_styles, styles) {
1140
+ return function(tag, additionalStyle = '') {
1141
+ if (inline_styles) {
1142
+ let style = styles[tag];
1143
+ if (!style && !additionalStyle) return '';
1144
+
1145
+ // When adding alignment that conflicts with the tag's default,
1146
+ // strip the default text-align first.
1147
+ if (additionalStyle && additionalStyle.includes('text-align') && style && style.includes('text-align')) {
1148
+ style = style.replace(/text-align:[^;]+;?/, '').trim();
1149
+ /* istanbul ignore next */
1150
+ if (style && !style.endsWith(';')) style += ';';
1151
+ }
1152
+
1153
+ /* istanbul ignore next - defensive: additionalStyle without style doesn't occur with current tags */
1154
+ const fullStyle = additionalStyle ? (style ? `${style}${additionalStyle}` : additionalStyle) : style;
1155
+ return ` style="${fullStyle}"`;
1156
+ } else {
1157
+ const classAttr = ` class="${CLASS_PREFIX}${tag}"`;
1158
+ if (additionalStyle) {
1159
+ return `${classAttr} style="${additionalStyle}"`;
1160
+ }
1161
+ return classAttr;
1162
+ }
1163
+ };
1164
+ }
1165
+
1166
+ // ════════════════════════════════════════════════════════════════════
1167
+ // Main parser function
1168
+ // ════════════════════════════════════════════════════════════════════
1169
+
1170
+ function quikdown(markdown, options = {}) {
1171
+ // ── Guard: only process non-empty strings ──
1172
+ if (!markdown || typeof markdown !== 'string') {
1173
+ return '';
1174
+ }
1175
+
1176
+ // ── Unpack options ──
1177
+ const { fence_plugin, inline_styles = false, bidirectional = false, lazy_linefeeds = false, allow_unsafe_html = false } = options;
1178
+ const styles = QUIKDOWN_STYLES;
1179
+ const getAttr = createGetAttr(inline_styles, styles);
1180
+
1181
+ // ── Helpers (closed over options) ──
1182
+
1183
+ /** Escape the five HTML-special characters. */
1184
+ function escapeHtml(text) {
1185
+ return text.replace(/[&<>"']/g, m => ESC_MAP[m]);
1186
+ }
1187
+
1188
+ /**
1189
+ * Bidirectional marker helper.
1190
+ * When bidirectional mode is on, returns ` data-qd="…"`.
1191
+ * The non-bidirectional branch is a trivial no-op arrow; it is
1192
+ * exercised in the core bundle but never in quikdown_bd.
1193
+ */
1194
+ /* istanbul ignore next - trivial no-op fallback */
1195
+ const dataQd = bidirectional ? (marker) => ` data-qd="${escapeHtml(marker)}"` : () => '';
1196
+
1197
+ /**
1198
+ * Sanitize a URL to block javascript:, vbscript:, and non-image data: URIs.
1199
+ * Returns '#' for blocked URLs.
1200
+ */
1201
+ function sanitizeUrl(url, allowUnsafe = false) {
1202
+ /* istanbul ignore next - defensive programming, regex ensures url is never empty */
1203
+ if (!url) return '';
1204
+ if (allowUnsafe) return url;
1205
+
1206
+ const trimmedUrl = url.trim();
1207
+ const lowerUrl = trimmedUrl.toLowerCase();
1208
+ const dangerousProtocols = ['javascript:', 'vbscript:', 'data:'];
1209
+
1210
+ for (const protocol of dangerousProtocols) {
1211
+ if (lowerUrl.startsWith(protocol)) {
1212
+ if (protocol === 'data:' && lowerUrl.startsWith('data:image/')) {
1213
+ return trimmedUrl;
1214
+ }
1215
+ return '#';
1216
+ }
1217
+ }
1218
+ return trimmedUrl;
1219
+ }
1220
+
1221
+ // ────────────────────────────────────────────────────────────────
1222
+ // Phase 1 — Code Extraction
1223
+ // ────────────────────────────────────────────────────────────────
1224
+ // Why extract code first? Fenced blocks and inline code spans can
1225
+ // contain markdown-like characters (*, _, #, |, etc.) that must NOT
1226
+ // be interpreted as formatting. By pulling them out and replacing
1227
+ // with unique placeholders, the rest of the pipeline never sees them.
1228
+
1229
+ let html = markdown;
1230
+ const codeBlocks = []; // Array of {lang, code, custom, fence, hasReverse}
1231
+ const inlineCodes = []; // Array of escaped-HTML strings
1232
+
1233
+ // ── Fenced code blocks ──
1234
+ // Matches paired fences: ``` with ``` and ~~~ with ~~~.
1235
+ // The fence must start at column 0 of a line (^ with /m flag).
1236
+ // Group 1 = fence marker, Group 2 = language hint, Group 3 = code body.
1237
+ html = html.replace(/^(```|~~~)([^\n]*)\n([\s\S]*?)^\1$/gm, (match, fence, lang, code) => {
1238
+ const placeholder = `${PLACEHOLDER_CB}${codeBlocks.length}§`;
1239
+ const langTrimmed = lang ? lang.trim() : '';
1240
+
1241
+ if (fence_plugin && fence_plugin.render && typeof fence_plugin.render === 'function') {
1242
+ // Custom plugin — store raw code (un-escaped) so the plugin
1243
+ // receives the original source.
1244
+ codeBlocks.push({
1245
+ lang: langTrimmed,
1246
+ code: code.trimEnd(),
1247
+ custom: true,
1248
+ fence: fence,
1249
+ hasReverse: !!fence_plugin.reverse
1250
+ });
1251
+ } else {
1252
+ // Default — pre-escape the code for safe HTML output.
1253
+ codeBlocks.push({
1254
+ lang: langTrimmed,
1255
+ code: escapeHtml(code.trimEnd()),
1256
+ custom: false,
1257
+ fence: fence
1258
+ });
1259
+ }
1260
+ return placeholder;
1261
+ });
1262
+
1263
+ // ── Inline code spans ──
1264
+ // Matches a single backtick pair: `content`.
1265
+ // Content is captured and HTML-escaped immediately.
1266
+ html = html.replace(/`([^`]+)`/g, (match, code) => {
1267
+ const placeholder = `${PLACEHOLDER_IC}${inlineCodes.length}§`;
1268
+ inlineCodes.push(escapeHtml(code));
1269
+ return placeholder;
1270
+ });
1271
+
1272
+ // ────────────────────────────────────────────────────────────────
1273
+ // Phase 2 — HTML Escaping
1274
+ // ────────────────────────────────────────────────────────────────
1275
+ // All remaining text (everything except code placeholders) is escaped
1276
+ // to prevent XSS. The `allow_unsafe_html` option skips this for
1277
+ // trusted pipelines that intentionally embed raw HTML.
1278
+
1279
+ if (!allow_unsafe_html) {
1280
+ html = escapeHtml(html);
1281
+ }
1282
+
1283
+ // ────────────────────────────────────────────────────────────────
1284
+ // Phase 3 — Block Scanning + Inline Formatting + Paragraphs
1285
+ // ────────────────────────────────────────────────────────────────
1286
+ // This is the heart of the lexer rewrite. Instead of applying
1287
+ // 10+ global regex passes, we:
1288
+ // 1. Process tables (line walker — tables need multi-line lookahead)
1289
+ // 2. Scan remaining lines for headings, HR, blockquotes
1290
+ // 3. Process lists (line walker — lists need indent tracking)
1291
+ // 4. Apply inline formatting to all text content
1292
+ // 5. Wrap remaining text in <p> tags
1293
+ //
1294
+ // Steps 1 and 3 are line-walkers that process the full text in a
1295
+ // single pass each. Step 2 replaces global regex with a per-line
1296
+ // scanner. Steps 4-5 are applied to the result.
1297
+ //
1298
+ // Total: 3 structured passes instead of 10+ regex passes.
1299
+
1300
+ // ── Step 1: Tables ──
1301
+ // Tables need multi-line lookahead (header → separator → body rows)
1302
+ // so they're handled by a dedicated line-walker first.
1303
+ html = processTable(html, getAttr);
1304
+
1305
+ // ── Step 2: Headings, HR, Blockquotes ──
1306
+ // These are simple line-level constructs. We scan each line once
1307
+ // and replace matching lines with their HTML representation.
1308
+ html = scanLineBlocks(html, getAttr, dataQd);
1309
+
1310
+ // ── Step 3: Lists ──
1311
+ // Lists need indent-level tracking across lines, so they get their
1312
+ // own line-walker.
1313
+ html = processLists(html, getAttr, inline_styles, bidirectional);
1314
+
1315
+ // ── Step 4: Inline formatting ──
1316
+ // Apply bold, italic, strikethrough, images, links, and autolinks
1317
+ // to all text content. This runs on the output of steps 1-3, so
1318
+ // it sees text inside headings, blockquotes, table cells, list
1319
+ // items, and paragraph text.
1320
+
1321
+ // Images (must come before links — ![alt](src) vs [text](url))
1322
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
1323
+ const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);
1324
+ /* istanbul ignore next - bd-only branch */
1325
+ const altAttr = bidirectional && alt ? ` data-qd-alt="${escapeHtml(alt)}"` : '';
1326
+ /* istanbul ignore next - bd-only branch */
1327
+ const srcAttr = bidirectional ? ` data-qd-src="${escapeHtml(src)}"` : '';
1328
+ return `<img${getAttr('img')} src="${sanitizedSrc}" alt="${alt}"${altAttr}${srcAttr}${dataQd('!')}>`;
1329
+ });
1330
+
1331
+ // Links
1332
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
1333
+ const sanitizedHref = sanitizeUrl(href, options.allow_unsafe_urls);
1334
+ const isExternal = /^https?:\/\//i.test(sanitizedHref);
1335
+ const rel = isExternal ? ' rel="noopener noreferrer"' : '';
1336
+ /* istanbul ignore next - bd-only branch */
1337
+ const textAttr = bidirectional ? ` data-qd-text="${escapeHtml(text)}"` : '';
1338
+ return `<a${getAttr('a')} href="${sanitizedHref}"${rel}${textAttr}${dataQd('[')}>${text}</a>`;
1339
+ });
1340
+
1341
+ // Autolinks — bare https?:// URLs become clickable <a> tags
1342
+ html = html.replace(/(^|\s)(https?:\/\/[^\s<]+)/g, (match, prefix, url) => {
1343
+ const sanitizedUrl = sanitizeUrl(url, options.allow_unsafe_urls);
1344
+ return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
1345
+ });
1346
+
1347
+ // Protect rendered tags so emphasis regexes don't see attribute
1348
+ // values — fixes #3 (underscores in URLs interpreted as emphasis).
1349
+ const savedTags = [];
1350
+ html = html.replace(/<[^>]+>/g, m => { savedTags.push(m); return `%%T${savedTags.length - 1}%%`; });
1351
+
1352
+ // Bold, italic, strikethrough
1353
+ const inlinePatterns = [
1354
+ [/\*\*(.+?)\*\*/g, 'strong', '**'],
1355
+ [/__(.+?)__/g, 'strong', '__'],
1356
+ [/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, 'em', '*'],
1357
+ [/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em', '_'],
1358
+ [/~~(.+?)~~/g, 'del', '~~']
1359
+ ];
1360
+ inlinePatterns.forEach(([pattern, tag, marker]) => {
1361
+ html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);
1362
+ });
1363
+
1364
+ // Restore protected tags
1365
+ html = html.replace(/%%T(\d+)%%/g, (_, i) => savedTags[i]);
1366
+
1367
+ // ── Step 5: Line breaks + paragraph wrapping ──
1368
+ if (lazy_linefeeds) {
1369
+ // Lazy linefeeds mode: every single \n becomes <br> EXCEPT:
1370
+ // • Double newlines → paragraph break
1371
+ // • Newlines adjacent to block elements (h, blockquote, pre, hr, table, list)
1372
+ //
1373
+ // Strategy: protect block-adjacent newlines with §N§, convert
1374
+ // the rest, then restore.
1375
+
1376
+ const blocks = [];
1377
+ let bi = 0;
1378
+
1379
+ // Protect tables and lists from <br> injection
1380
+ html = html.replace(/<(table|[uo]l)[^>]*>[\s\S]*?<\/\1>/g, m => {
1381
+ blocks[bi] = m;
1382
+ return `§B${bi++}§`;
1383
+ });
1384
+
1385
+ html = html.replace(/\n\n+/g, '§P§')
1386
+ // After block-level closing tags
1387
+ .replace(/(<\/(?:h[1-6]|blockquote|pre)>)\n/g, '$1§N§')
1388
+ .replace(/(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)\n/g, '$1§N§')
1389
+ // Before block-level opening tags
1390
+ .replace(/\n(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)/g, '§N§$1')
1391
+ .replace(/\n(§B\d+§)/g, '§N§$1')
1392
+ .replace(/(§B\d+§)\n/g, '$1§N§')
1393
+ // Convert surviving newlines to <br>
1394
+ .replace(/\n/g, `<br${getAttr('br')}>`)
1395
+ // Restore
1396
+ .replace(/§N§/g, '\n')
1397
+ .replace(/§P§/g, '</p><p>');
1398
+
1399
+ // Restore protected blocks
1400
+ blocks.forEach((b, i) => html = html.replace(`§B${i}§`, b));
1401
+
1402
+ html = '<p>' + html + '</p>';
1403
+ } else {
1404
+ // Standard mode: two trailing spaces → <br>, double newline → new paragraph
1405
+ html = html.replace(/ {2}$/gm, `<br${getAttr('br')}>`);
1406
+
1407
+ html = html.replace(/\n\n+/g, (match, offset) => {
1408
+ const before = html.substring(0, offset);
1409
+ if (before.match(/<\/(h[1-6]|blockquote|ul|ol|table|pre|hr)>$/)) {
1410
+ return '<p>';
1411
+ }
1412
+ return '</p><p>';
1413
+ });
1414
+ html = '<p>' + html + '</p>';
1415
+ }
1416
+
1417
+ // ── Step 6: Cleanup ──
1418
+ // Remove <p> wrappers that accidentally enclose block elements.
1419
+ // This is simpler than trying to prevent them during wrapping.
1420
+ const cleanupPatterns = [
1421
+ [/<p><\/p>/g, ''],
1422
+ [/<p>(<h[1-6][^>]*>)/g, '$1'],
1423
+ [/(<\/h[1-6]>)<\/p>/g, '$1'],
1424
+ [/<p>(<blockquote[^>]*>)/g, '$1'],
1425
+ [/(<\/blockquote>)<\/p>/g, '$1'],
1426
+ [/<p>(<ul[^>]*>|<ol[^>]*>)/g, '$1'],
1427
+ [/(<\/ul>|<\/ol>)<\/p>/g, '$1'],
1428
+ [/<p>(<hr[^>]*>)<\/p>/g, '$1'],
1429
+ [/<p>(<table[^>]*>)/g, '$1'],
1430
+ [/(<\/table>)<\/p>/g, '$1'],
1431
+ [/<p>(<pre[^>]*>)/g, '$1'],
1432
+ [/(<\/pre>)<\/p>/g, '$1'],
1433
+ [new RegExp(`<p>(${PLACEHOLDER_CB}\\d+§)</p>`, 'g'), '$1']
1434
+ ];
1435
+ cleanupPatterns.forEach(([pattern, replacement]) => {
1436
+ html = html.replace(pattern, replacement);
1437
+ });
1438
+
1439
+ // When a block element is followed by a newline and then text, open a <p>.
1440
+ html = html.replace(/(<\/(?:h[1-6]|blockquote|ul|ol|table|pre|hr)>)\n([^<])/g, '$1\n<p>$2');
1441
+
1442
+ // ────────────────────────────────────────────────────────────────
1443
+ // Phase 4 — Code Restoration
1444
+ // ────────────────────────────────────────────────────────────────
1445
+ // Replace placeholders with rendered HTML. For fenced blocks this
1446
+ // means wrapping in <pre><code>…</code></pre> (or calling the
1447
+ // fence_plugin). For inline code it means <code>…</code>.
1448
+
1449
+ codeBlocks.forEach((block, i) => {
1450
+ let replacement;
1451
+
1452
+ if (block.custom && fence_plugin && fence_plugin.render) {
1453
+ // Delegate to the user-provided fence plugin.
1454
+ replacement = fence_plugin.render(block.code, block.lang);
1455
+
1456
+ if (replacement === undefined) {
1457
+ // Plugin declined — fall back to default rendering.
1458
+ const langClass = !inline_styles && block.lang ? ` class="language-${block.lang}"` : '';
1459
+ const codeAttr = inline_styles ? getAttr('code') : langClass;
1460
+ /* istanbul ignore next - bd-only branch */
1461
+ const langAttr = bidirectional && block.lang ? ` data-qd-lang="${escapeHtml(block.lang)}"` : '';
1462
+ /* istanbul ignore next - bd-only branch */
1463
+ const fenceAttr = bidirectional ? ` data-qd-fence="${escapeHtml(block.fence)}"` : '';
1464
+ replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${escapeHtml(block.code)}</code></pre>`;
1465
+ } else /* istanbul ignore next - bd-only branch */ if (bidirectional) {
1466
+ // Plugin returned HTML — inject data attributes for roundtrip.
1467
+ replacement = replacement.replace(/^<(\w+)/,
1468
+ `<$1 data-qd-fence="${escapeHtml(block.fence)}" data-qd-lang="${escapeHtml(block.lang)}" data-qd-source="${escapeHtml(block.code)}"`);
1469
+ }
1470
+ } else {
1471
+ // Default rendering — wrap in <pre><code>.
1472
+ const langClass = !inline_styles && block.lang ? ` class="language-${block.lang}"` : '';
1473
+ const codeAttr = inline_styles ? getAttr('code') : langClass;
1474
+ /* istanbul ignore next - bd-only branch */
1475
+ const langAttr = bidirectional && block.lang ? ` data-qd-lang="${escapeHtml(block.lang)}"` : '';
1476
+ /* istanbul ignore next - bd-only branch */
1477
+ const fenceAttr = bidirectional ? ` data-qd-fence="${escapeHtml(block.fence)}"` : '';
1478
+ replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${block.code}</code></pre>`;
1479
+ }
1480
+
1481
+ const placeholder = `${PLACEHOLDER_CB}${i}§`;
1482
+ html = html.replace(placeholder, replacement);
1483
+ });
1484
+
1485
+ // Restore inline code spans
1486
+ inlineCodes.forEach((code, i) => {
1487
+ const placeholder = `${PLACEHOLDER_IC}${i}§`;
1488
+ html = html.replace(placeholder, `<code${getAttr('code')}${dataQd('`')}>${code}</code>`);
1489
+ });
1490
+
1491
+ return html.trim();
1492
+ }
1493
+
1494
+ // ════════════════════════════════════════════════════════════════════
1495
+ // Block-level line scanner
1496
+ // ════════════════════════════════════════════════════════════════════
1497
+
1498
+ /**
1499
+ * scanLineBlocks — single-pass line scanner for headings, HR, blockquotes
1500
+ *
1501
+ * Walks the text line by line. For each line it checks (in order):
1502
+ * 1. Heading — starts with 1-6 '#' followed by a space
1503
+ * 2. HR — line is entirely '---…' (3+ dashes, optional trailing space)
1504
+ * 3. Blockquote — starts with '&gt; ' (the > was already HTML-escaped)
1505
+ *
1506
+ * Lines that don't match any block pattern are passed through unchanged.
1507
+ *
1508
+ * This replaces three separate global regex passes from the pre-1.2.8
1509
+ * architecture with one structured scan.
1510
+ *
1511
+ * @param {string} text The document text (HTML-escaped, code extracted)
1512
+ * @param {Function} getAttr Attribute factory (class or style)
1513
+ * @param {Function} dataQd Bidirectional marker factory
1514
+ * @returns {string} Text with block-level elements rendered
1515
+ */
1516
+ function scanLineBlocks(text, getAttr, dataQd) {
1517
+ const lines = text.split('\n');
1518
+ const result = [];
1519
+ let i = 0;
1520
+
1521
+ while (i < lines.length) {
1522
+ const line = lines[i];
1523
+
1524
+ // ── Heading ──
1525
+ // Count leading '#' characters. Valid heading: 1-6 hashes then a space.
1526
+ // Example: "## Hello World ##" → <h2>Hello World</h2>
1527
+ let hashCount = 0;
1528
+ while (hashCount < line.length && hashCount < 7 && line[hashCount] === '#') {
1529
+ hashCount++;
1530
+ }
1531
+ if (hashCount >= 1 && hashCount <= 6 && line[hashCount] === ' ') {
1532
+ // Extract content after "# " and strip trailing hashes
1533
+ const content = line.slice(hashCount + 1).replace(/\s*#+\s*$/, '');
1534
+ const tag = 'h' + hashCount;
1535
+ result.push(`<${tag}${getAttr(tag)}${dataQd('#'.repeat(hashCount))}>${content}</${tag}>`);
1536
+ i++;
1537
+ continue;
1538
+ }
1539
+
1540
+ // ── Horizontal Rule ──
1541
+ // Three or more dashes, optional trailing whitespace, nothing else.
1542
+ if (isDashHRLine(line)) {
1543
+ result.push(`<hr${getAttr('hr')}>`);
1544
+ i++;
1545
+ continue;
1546
+ }
1547
+
1548
+ // ── Blockquote ──
1549
+ // After Phase 2, the '>' character has been escaped to '&gt;'.
1550
+ // Pattern: "&gt; content" or merged consecutive blockquotes.
1551
+ if (/^&gt;\s+/.test(line)) {
1552
+ result.push(`<blockquote${getAttr('blockquote')}>${line.replace(/^&gt;\s+/, '')}</blockquote>`);
1553
+ i++;
1554
+ continue;
1555
+ }
1556
+
1557
+ // ── Pass-through ──
1558
+ result.push(line);
1559
+ i++;
1560
+ }
1561
+
1562
+ // Merge consecutive blockquotes into a single element.
1563
+ // <blockquote>A</blockquote>\n<blockquote>B</blockquote>
1564
+ // → <blockquote>A\nB</blockquote>
1565
+ let joined = result.join('\n');
1566
+ joined = joined.replace(/<\/blockquote>\n<blockquote>/g, '\n');
1567
+ return joined;
1568
+ }
1569
+
1570
+ // ════════════════════════════════════════════════════════════════════
1571
+ // Table processing (line walker)
1572
+ // ════════════════════════════════════════════════════════════════════
1573
+
1574
+ /**
1575
+ * Inline markdown formatter for table cells.
1576
+ * Handles bold, italic, strikethrough, and code within cell text.
1577
+ * Links / images / autolinks are handled by the global inline pass
1578
+ * (Phase 3 Step 4) which runs after table processing.
1579
+ */
1580
+ function processInlineMarkdown(text, getAttr) {
1581
+ const patterns = [
1582
+ [/\*\*(.+?)\*\*/g, 'strong'],
1583
+ [/__(.+?)__/g, 'strong'],
1584
+ [/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, 'em'],
1585
+ [/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em'],
1586
+ [/~~(.+?)~~/g, 'del'],
1587
+ [/`([^`]+)`/g, 'code']
1588
+ ];
1589
+ patterns.forEach(([pattern, tag]) => {
1590
+ text = text.replace(pattern, `<${tag}${getAttr(tag)}>$1</${tag}>`);
1591
+ });
1592
+ return text;
1593
+ }
1594
+
1595
+ /**
1596
+ * processTable — line walker for markdown tables
1597
+ *
1598
+ * Walks through lines looking for runs of pipe-containing lines.
1599
+ * Each run is validated (must contain a separator row: |---|---|)
1600
+ * and rendered as an HTML <table>. Invalid runs are restored as-is.
1601
+ *
1602
+ * @param {string} text Full document text
1603
+ * @param {Function} getAttr Attribute factory
1604
+ * @returns {string} Text with tables rendered
1605
+ */
1606
+ function processTable(text, getAttr) {
1607
+ const lines = text.split('\n');
1608
+ const result = [];
1609
+ let inTable = false;
1610
+ let tableLines = [];
1611
+
1612
+ for (let i = 0; i < lines.length; i++) {
1613
+ const line = lines[i].trim();
1614
+
1615
+ if (line.includes('|') && (line.startsWith('|') || /[^\\|]/.test(line))) {
1616
+ if (!inTable) {
1617
+ inTable = true;
1618
+ tableLines = [];
1619
+ }
1620
+ tableLines.push(line);
1621
+ } else {
1622
+ if (inTable) {
1623
+ const tableHtml = buildTable(tableLines, getAttr);
1624
+ if (tableHtml) {
1625
+ result.push(tableHtml);
1626
+ } else {
1627
+ result.push(...tableLines);
1628
+ }
1629
+ inTable = false;
1630
+ tableLines = [];
1631
+ }
1632
+ result.push(lines[i]);
1633
+ }
1634
+ }
1635
+
1636
+ // Handle table at end of document
1637
+ if (inTable && tableLines.length > 0) {
1638
+ const tableHtml = buildTable(tableLines, getAttr);
1639
+ if (tableHtml) {
1640
+ result.push(tableHtml);
1641
+ } else {
1642
+ result.push(...tableLines);
1643
+ }
1644
+ }
1645
+
1646
+ return result.join('\n');
1647
+ }
1648
+
1649
+ /**
1650
+ * buildTable — validate and render a table from accumulated lines
1651
+ *
1652
+ * @param {string[]} lines Array of pipe-containing lines
1653
+ * @param {Function} getAttr Attribute factory
1654
+ * @returns {string|null} HTML table string, or null if invalid
1655
+ */
1656
+ function buildTable(lines, getAttr) {
1657
+ if (lines.length < 2) return null;
1658
+
1659
+ // Find the separator row (---|---|)
1660
+ let separatorIndex = -1;
1661
+ for (let i = 1; i < lines.length; i++) {
1662
+ if (/^\|?[\s\-:|]+\|?$/.test(lines[i]) && lines[i].includes('-')) {
1663
+ separatorIndex = i;
1664
+ break;
1665
+ }
1666
+ }
1667
+ if (separatorIndex === -1) return null;
1668
+
1669
+ const headerLines = lines.slice(0, separatorIndex);
1670
+ const bodyLines = lines.slice(separatorIndex + 1);
1671
+
1672
+ // Parse alignment from separator cells (:--- = left, :---: = center, ---: = right)
1673
+ const separator = lines[separatorIndex];
1674
+ const separatorCells = separator.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
1675
+ const alignments = separatorCells.map(cell => {
1676
+ const trimmed = cell.trim();
1677
+ if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';
1678
+ if (trimmed.endsWith(':')) return 'right';
1679
+ return 'left';
1680
+ });
1681
+
1682
+ let html = `<table${getAttr('table')}>\n`;
1683
+
1684
+ // Header
1685
+ html += `<thead${getAttr('thead')}>\n`;
1686
+ headerLines.forEach(line => {
1687
+ html += `<tr${getAttr('tr')}>\n`;
1688
+ const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
1689
+ cells.forEach((cell, i) => {
1690
+ const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
1691
+ const processedCell = processInlineMarkdown(cell.trim(), getAttr);
1692
+ html += `<th${getAttr('th', alignStyle)}>${processedCell}</th>\n`;
1693
+ });
1694
+ html += '</tr>\n';
1695
+ });
1696
+ html += '</thead>\n';
1697
+
1698
+ // Body
1699
+ if (bodyLines.length > 0) {
1700
+ html += `<tbody${getAttr('tbody')}>\n`;
1701
+ bodyLines.forEach(line => {
1702
+ html += `<tr${getAttr('tr')}>\n`;
1703
+ const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
1704
+ cells.forEach((cell, i) => {
1705
+ const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
1706
+ const processedCell = processInlineMarkdown(cell.trim(), getAttr);
1707
+ html += `<td${getAttr('td', alignStyle)}>${processedCell}</td>\n`;
1708
+ });
1709
+ html += '</tr>\n';
1710
+ });
1711
+ html += '</tbody>\n';
1712
+ }
1713
+
1714
+ html += '</table>';
1715
+ return html;
1716
+ }
1717
+
1718
+ // ════════════════════════════════════════════════════════════════════
1719
+ // List processing (line walker)
1720
+ // ════════════════════════════════════════════════════════════════════
1721
+
1722
+ /**
1723
+ * processLists — line walker for ordered, unordered, and task lists
1724
+ *
1725
+ * Scans each line for list markers (-, *, +, 1., 2., etc.) with
1726
+ * optional leading indentation for nesting. Non-list lines close
1727
+ * any open lists and pass through unchanged.
1728
+ *
1729
+ * Task lists (- [ ] / - [x]) are detected and rendered with
1730
+ * checkbox inputs.
1731
+ *
1732
+ * @param {string} text Full document text
1733
+ * @param {Function} getAttr Attribute factory
1734
+ * @param {boolean} inline_styles Whether to use inline styles
1735
+ * @param {boolean} bidirectional Whether to add data-qd markers
1736
+ * @returns {string} Text with lists rendered
1737
+ */
1738
+ function processLists(text, getAttr, inline_styles, bidirectional) {
1739
+ const lines = text.split('\n');
1740
+ const result = [];
1741
+ const listStack = []; // tracks nesting: [{type:'ul', level:0}, …]
1742
+
1743
+ // Helper to escape HTML for data-qd attributes. List markers (`-`, `*`,
1744
+ // `+`, `1.`, etc.) never contain HTML-special chars, so the replace
1745
+ // callback is defensive-only and never actually fires in practice.
1746
+ /* istanbul ignore next - defensive: list markers never trigger escaping */
1747
+ const escapeHtml = (text) => text.replace(/[&<>"']/g,
1748
+ /* istanbul ignore next - defensive: list markers never contain HTML specials */
1749
+ m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[m]);
1750
+ /* istanbul ignore next - trivial no-op fallback; not exercised via bd bundle */
1751
+ const dataQd = bidirectional ? (marker) => ` data-qd="${escapeHtml(marker)}"` : () => '';
1752
+
1753
+ for (let i = 0; i < lines.length; i++) {
1754
+ const line = lines[i];
1755
+ const match = line.match(/^(\s*)([*\-+]|\d+\.)\s+(.+)$/);
1756
+
1757
+ if (match) {
1758
+ const [, indent, marker, content] = match;
1759
+ const level = Math.floor(indent.length / 2);
1760
+ const isOrdered = /^\d+\./.test(marker);
1761
+ const listType = isOrdered ? 'ol' : 'ul';
1762
+
1763
+ // Task list detection (only in unordered lists)
1764
+ let listItemContent = content;
1765
+ let taskListClass = '';
1766
+ const taskMatch = content.match(/^\[([x ])\]\s+(.*)$/i);
1767
+ if (taskMatch && !isOrdered) {
1768
+ const [, checked, taskContent] = taskMatch;
1769
+ const isChecked = checked.toLowerCase() === 'x';
1770
+ const checkboxAttr = inline_styles
1771
+ ? ' style="margin-right:.5em"'
1772
+ : ` class="${CLASS_PREFIX}task-checkbox"`;
1773
+ listItemContent = `<input type="checkbox"${checkboxAttr}${isChecked ? ' checked' : ''} disabled> ${taskContent}`;
1774
+ taskListClass = inline_styles ? ' style="list-style:none"' : ` class="${CLASS_PREFIX}task-item"`;
1775
+ }
1776
+
1777
+ // Close deeper nesting levels
1778
+ while (listStack.length > level + 1) {
1779
+ const list = listStack.pop();
1780
+ result.push(`</${list.type}>`);
1781
+ }
1782
+
1783
+ // Open new list or switch type at current level
1784
+ if (listStack.length === level) {
1785
+ listStack.push({ type: listType, level });
1786
+ result.push(`<${listType}${getAttr(listType)}>`);
1787
+ } else if (listStack.length === level + 1) {
1788
+ const currentList = listStack[listStack.length - 1];
1789
+ if (currentList.type !== listType) {
1790
+ result.push(`</${currentList.type}>`);
1791
+ listStack.pop();
1792
+ listStack.push({ type: listType, level });
1793
+ result.push(`<${listType}${getAttr(listType)}>`);
1794
+ }
1795
+ }
1796
+
1797
+ const liAttr = taskListClass || getAttr('li');
1798
+ result.push(`<li${liAttr}${dataQd(marker)}>${listItemContent}</li>`);
1799
+ } else {
1800
+ // Not a list item — close all open lists
1801
+ while (listStack.length > 0) {
1802
+ const list = listStack.pop();
1803
+ result.push(`</${list.type}>`);
1804
+ }
1805
+ result.push(line);
1806
+ }
1807
+ }
1808
+
1809
+ // Close any remaining open lists
1810
+ while (listStack.length > 0) {
1811
+ const list = listStack.pop();
1812
+ result.push(`</${list.type}>`);
1813
+ }
1814
+
1815
+ return result.join('\n');
1816
+ }
1817
+
1818
+ // ════════════════════════════════════════════════════════════════════
1819
+ // Static API
1820
+ // ════════════════════════════════════════════════════════════════════
1821
+
1822
+ /**
1823
+ * Emit CSS rules for all quikdown elements.
1824
+ *
1825
+ * @param {string} prefix Class prefix (default: 'quikdown-')
1826
+ * @param {string} theme 'light' (default) or 'dark'
1827
+ * @returns {string} CSS text
1828
+ */
1829
+ quikdown.emitStyles = function(prefix = 'quikdown-', theme = 'light') {
1830
+ const styles = QUIKDOWN_STYLES;
1831
+
1832
+ const themeOverrides = {
1833
+ dark: {
1834
+ '#f4f4f4': '#2a2a2a', // pre background
1835
+ '#f0f0f0': '#2a2a2a', // code background
1836
+ '#f2f2f2': '#2a2a2a', // th background
1837
+ '#ddd': '#3a3a3a', // borders
1838
+ '#06c': '#6db3f2', // links
1839
+ _textColor: '#e0e0e0'
1840
+ },
1841
+ light: {
1842
+ _textColor: '#333'
1843
+ }
1844
+ };
1845
+
1846
+ let css = '';
1847
+ for (const [tag, style] of Object.entries(styles)) {
1848
+ let themedStyle = style;
1849
+
1850
+ if (theme === 'dark' && themeOverrides.dark) {
1851
+ for (const [oldColor, newColor] of Object.entries(themeOverrides.dark)) {
1852
+ if (!oldColor.startsWith('_')) {
1853
+ themedStyle = themedStyle.replaceAll(oldColor, newColor);
1854
+ }
1855
+ }
1856
+ const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
1857
+ if (needsTextColor.includes(tag)) {
1858
+ themedStyle += `;color:${themeOverrides.dark._textColor}`;
1859
+ }
1860
+ } else if (theme === 'light' && themeOverrides.light) {
1861
+ const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
1862
+ if (needsTextColor.includes(tag)) {
1863
+ themedStyle += `;color:${themeOverrides.light._textColor}`;
1864
+ }
1865
+ }
1866
+
1867
+ css += `.${prefix}${tag} { ${themedStyle} }\n`;
1868
+ }
1869
+
1870
+ return css;
1871
+ };
1872
+
1873
+ /**
1874
+ * Create a pre-configured parser with baked-in options.
1875
+ *
1876
+ * @param {Object} options Options to bake in
1877
+ * @returns {Function} Configured quikdown(markdown) function
1878
+ */
1879
+ quikdown.configure = function(options) {
1880
+ return function(markdown) {
1881
+ return quikdown(markdown, options);
1882
+ };
1883
+ };
1884
+
1885
+ /** Semantic version (injected at build time) */
1886
+ quikdown.version = quikdownVersion;
1887
+
1888
+ // ════════════════════════════════════════════════════════════════════
1889
+ // Exports
1890
+ // ════════════════════════════════════════════════════════════════════
1891
+
1892
+ /* istanbul ignore next */
1893
+ if (typeof module !== 'undefined' && module.exports) {
1894
+ module.exports = quikdown;
1895
+ }
1896
+
1897
+ /* istanbul ignore next */
1898
+ if (typeof window !== 'undefined') {
1899
+ window.quikdown = quikdown;
1900
+ }
1901
+
1902
+ // Subclass that pre-wires quikdown as the message formatter
1903
+ var quikchatMD = /*#__PURE__*/function (_quikchat) {
1904
+ function quikchatMD(parentElement, onSend) {
1905
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
1906
+ _classCallCheck(this, quikchatMD);
1907
+ if (!options.messageFormatter) {
1908
+ options.messageFormatter = function (content) {
1909
+ return quikdown(content);
1910
+ };
1911
+ }
1912
+ return _callSuper(this, quikchatMD, [parentElement, onSend, options]);
1913
+ }
1914
+ _inherits(quikchatMD, _quikchat);
1915
+ return _createClass(quikchatMD);
1916
+ }(quikchat); // Expose quikdown on the class for direct access
1917
+ quikchatMD.quikdown = quikdown;
1918
+
1919
+ return quikchatMD;
1920
+
1921
+ }));
1922
+ //# sourceMappingURL=quikchat-md.umd.js.map