@vanduo-oss/framework 1.4.2 → 1.4.3

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.
@@ -566,19 +566,65 @@
566
566
  * @returns {string} HTML with syntax highlighting spans
567
567
  */
568
568
  highlightHtml: function (html) {
569
- // Highlight HTML tags
570
- html = html.replace(/(&lt;\/?)([\w-]+)/g, '$1<span class="code-tag">$2</span>');
569
+ // Comments first so tag highlighting does not treat them as elements.
570
+ html = html.replace(/(&lt;!--)(.*?)(--&gt;)/g, '<span class="code-comment">$1$2$3</span>');
571
571
 
572
- // Highlight attributes
573
- html = html.replace(/([\w-]+)(=)(["'])/g, '<span class="code-attr">$1</span>$2$3');
574
- html = html.replace(/([\w-]+)(=)(&quot;|&#39;)/g, '<span class="code-attr">$1</span>$2$3');
572
+ // Only highlight inside escaped HTML tags. Build the highlighted tag from the
573
+ // original escaped source so regexes never re-process our injected spans.
574
+ html = html.replace(/&lt;(?!--)([\s\S]*?)&gt;/g, function (match, inner) {
575
+ const closingMatch = inner.match(/^(\/)([\w-]+)\s*$/);
576
+ if (closingMatch) {
577
+ return '&lt;'
578
+ + closingMatch[1]
579
+ + '<span class="code-tag">' + closingMatch[2] + '</span>'
580
+ + '&gt;';
581
+ }
575
582
 
576
- // Highlight attribute values (strings)
577
- html = html.replace(/(["'])([^"']*)(["'])/g, '$1<span class="code-string">$2</span>$3');
578
- html = html.replace(/(&quot;|&#39;)([^&]*)(&quot;|&#39;)/g, '$1<span class="code-string">$2</span>$3');
583
+ const selfClosingMatch = inner.match(/(\s*\/)\s*$/);
584
+ const selfClosingSuffix = selfClosingMatch ? selfClosingMatch[1] : '';
585
+ const tagSource = selfClosingMatch
586
+ ? inner.slice(0, inner.length - selfClosingMatch[0].length)
587
+ : inner;
588
+ const openMatch = tagSource.match(/^([\w-]+)([\s\S]*)$/);
579
589
 
580
- // Highlight comments
581
- html = html.replace(/(&lt;!--)(.*?)(--&gt;)/g, '<span class="code-comment">$1$2$3</span>');
590
+ if (!openMatch) {
591
+ return match;
592
+ }
593
+
594
+ const tagName = openMatch[1];
595
+ let attrSource = openMatch[2];
596
+
597
+ attrSource = attrSource.replace(
598
+ /([\w:-]+)(\s*=\s*)(["'])([\s\S]*?)\3/g,
599
+ function (attrMatch, attrName, separator, quote, value) {
600
+ return '<span class="code-attr">' + attrName + '</span>'
601
+ + separator
602
+ + quote
603
+ + '<span class="code-string">' + value + '</span>'
604
+ + quote;
605
+ }
606
+ );
607
+
608
+ attrSource = attrSource.replace(
609
+ /([\w:-]+)(\s*=\s*)(&quot;|&#39;)([\s\S]*?)(&quot;|&#39;)/g,
610
+ function (attrMatch, attrName, separator, openQuote, value, closeQuote) {
611
+ if (openQuote !== closeQuote) {
612
+ return attrMatch;
613
+ }
614
+
615
+ return '<span class="code-attr">' + attrName + '</span>'
616
+ + separator
617
+ + openQuote
618
+ + '<span class="code-string">' + value + '</span>'
619
+ + closeQuote;
620
+ }
621
+ );
622
+
623
+ return '&lt;<span class="code-tag">' + tagName + '</span>'
624
+ + attrSource
625
+ + selfClosingSuffix
626
+ + '&gt;';
627
+ });
582
628
 
583
629
  return html;
584
630
  },
@@ -613,6 +659,20 @@
613
659
  * @returns {string} JS with syntax highlighting spans
614
660
  */
615
661
  highlightJs: function (js) {
662
+ const protectedTokens = [];
663
+ const protectToken = (value, className) => {
664
+ const marker = String.fromCharCode(0xE000 + protectedTokens.length);
665
+ protectedTokens.push({ marker, html: '<span class="' + className + '">' + value + '</span>' });
666
+ return marker;
667
+ };
668
+
669
+ // Protect comments and strings before adding markup so later passes never
670
+ // re-highlight attributes inside the generated spans.
671
+ js = js.replace(
672
+ /\/\*[\s\S]*?\*\/|\/\/.*$|'(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`/gm,
673
+ match => protectToken(match, match.startsWith('/') ? 'code-comment' : 'code-string')
674
+ );
675
+
616
676
  // Highlight keywords
617
677
  const keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue', 'new', 'this', 'class', 'extends', 'import', 'export', 'default', 'async', 'await', 'try', 'catch', 'throw', 'typeof', 'instanceof'];
618
678
  keywords.forEach(kw => {
@@ -620,18 +680,15 @@
620
680
  js = js.replace(regex, '<span class="code-keyword">$1</span>');
621
681
  });
622
682
 
623
- // Highlight strings (limit to 10 000 chars to prevent polynomial backtracking)
624
- js = js.replace(/('(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`)/g, '<span class="code-string">$1</span>');
625
-
626
683
  // Highlight numbers
627
684
  js = js.replace(/\b(\d+\.?\d*)\b/g, '<span class="code-number">$1</span>');
628
685
 
629
686
  // Highlight function calls
630
687
  js = js.replace(/\b([\w]+)(\s*\()/g, '<span class="code-function">$1</span>$2');
631
688
 
632
- // Highlight comments
633
- js = js.replace(/(\/\/.*$)/gm, '<span class="code-comment">$1</span>');
634
- js = js.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="code-comment">$1</span>');
689
+ protectedTokens.forEach(token => {
690
+ js = js.replace(token.marker, token.html);
691
+ });
635
692
 
636
693
  return js;
637
694
  },
@@ -181,6 +181,8 @@
181
181
  let focusedDate = null;
182
182
  /** Prevents focus() after close from immediately re-opening the popup */
183
183
  let skipNextFocusOpen = false;
184
+ /** Ignore outside-click closes briefly after open (mobile click/focus ordering). */
185
+ let ignoreOutsideUntil = 0;
184
186
 
185
187
  const isDisabled = (d) => {
186
188
  if (minDate) {
@@ -562,7 +564,12 @@
562
564
  positionPopup();
563
565
  };
564
566
 
567
+ const markIgnoreOutside = () => {
568
+ ignoreOutsideUntil = Date.now() + 100;
569
+ };
570
+
565
571
  const open = () => {
572
+ markIgnoreOutside();
566
573
  viewMode = 'days';
567
574
  if (selectedDate) {
568
575
  viewYear = selectedDate.getFullYear();
@@ -600,8 +607,25 @@
600
607
  }
601
608
  open();
602
609
  };
610
+ /** Mobile browsers may not focus the input before the click event (Playwright too). */
611
+ const clickHandler = () => {
612
+ if (!popup.classList.contains('is-open')) open();
613
+ };
614
+
615
+ const isOpenAnchorTarget = (target) => {
616
+ if (!target || !(target instanceof Node)) return false;
617
+ if (target === input || input.contains(target) || popup.contains(target)) return true;
618
+ const inputId = input.id;
619
+ if (inputId) {
620
+ const label = document.querySelector('label[for="' + inputId.replace(/"/g, '\\"') + '"]');
621
+ if (label && (target === label || label.contains(target))) return true;
622
+ }
623
+ return false;
624
+ };
625
+
603
626
  const outsideHandler = (e) => {
604
- if (!input.contains(e.target) && !popup.contains(e.target)) close();
627
+ if (Date.now() < ignoreOutsideUntil) return;
628
+ if (!isOpenAnchorTarget(e.target)) close();
605
629
  };
606
630
  const escHandler = (e) => {
607
631
  if (e.key === 'Escape' && popup.classList.contains('is-open')) {
@@ -612,6 +636,7 @@
612
636
  };
613
637
 
614
638
  input.addEventListener('focus', focusHandler);
639
+ input.addEventListener('click', clickHandler);
615
640
  document.addEventListener('click', outsideHandler, true);
616
641
  document.addEventListener('keydown', escHandler);
617
642
  popup.addEventListener('keydown', handleGridKeydown);
@@ -624,6 +649,7 @@
624
649
 
625
650
  cleanup.push(
626
651
  () => input.removeEventListener('focus', focusHandler),
652
+ () => input.removeEventListener('click', clickHandler),
627
653
  () => document.removeEventListener('click', outsideHandler, true),
628
654
  () => document.removeEventListener('keydown', escHandler),
629
655
  () => popup.removeEventListener('keydown', handleGridKeydown),
@@ -6,17 +6,6 @@
6
6
  (function () {
7
7
  'use strict';
8
8
 
9
- /**
10
- * Escape HTML entities to prevent XSS when inserting into innerHTML
11
- * @param {string} text
12
- * @returns {string}
13
- */
14
- function _escapeHtml(text) {
15
- const div = document.createElement('div');
16
- div.textContent = text;
17
- return div.innerHTML;
18
- }
19
-
20
9
  /**
21
10
  * Allow same-origin URLs by default, or an explicit allowlist of origins.
22
11
  * @param {string} url
@@ -109,10 +98,24 @@
109
98
 
110
99
  const text = typeof item === 'object' ? (item.label || item.text || String(item)) : String(item);
111
100
  if (query) {
112
- // Escape HTML first to prevent XSS, then highlight matches in the safe string
113
- const escaped = _escapeHtml(text);
114
- const re = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
115
- li.innerHTML = escaped.replace(re, '<span class="vd-suggest-match">$1</span>');
101
+ const lowerText = text.toLowerCase();
102
+ const lowerQuery = query.toLowerCase();
103
+ let start = 0;
104
+ let matchIndex = lowerText.indexOf(lowerQuery, start);
105
+ while (matchIndex !== -1) {
106
+ if (matchIndex > start) {
107
+ li.appendChild(document.createTextNode(text.slice(start, matchIndex)));
108
+ }
109
+ const matchSpan = document.createElement('span');
110
+ matchSpan.className = 'vd-suggest-match';
111
+ matchSpan.textContent = text.slice(matchIndex, matchIndex + query.length);
112
+ li.appendChild(matchSpan);
113
+ start = matchIndex + query.length;
114
+ matchIndex = lowerText.indexOf(lowerQuery, start);
115
+ }
116
+ if (start < text.length) {
117
+ li.appendChild(document.createTextNode(text.slice(start)));
118
+ }
116
119
  } else {
117
120
  li.textContent = text;
118
121
  }
package/js/index.js CHANGED
@@ -68,7 +68,6 @@ import './components/rating.js';
68
68
  import './components/transfer.js';
69
69
  import './components/tree.js';
70
70
  import './components/spotlight.js';
71
- import './components/music-player.js';
72
71
 
73
72
  // Re-export for ESM / CJS consumers
74
73
  const Vanduo = window.Vanduo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanduo-oss/framework",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Zero-dependency CSS/JS framework built on Fibonacci/Golden Ratio design system with Open Color integration",
5
5
  "keywords": [
6
6
  "css",