| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
function tryRegex(pattern, flags) { |
| 5 |
try { |
| 6 |
if (!pattern) return { re: null, error: null }; |
| 7 |
return { re: new RegExp(pattern, flags), error: null }; |
| 8 |
} catch(e) { |
| 9 |
return { re: null, error: e.message }; |
| 10 |
} |
| 11 |
} |
| 12 |
|
| 13 |
function getMatches(re, str) { |
| 14 |
if (!re || !str) return []; |
| 15 |
var matches = [], m; |
| 16 |
var safeFlags = re.flags; |
| 17 |
if (!safeFlags.includes('g')) safeFlags += 'g'; |
| 18 |
var r; |
| 19 |
try { r = new RegExp(re.source, safeFlags); } catch(e) { return []; } |
| 20 |
var watchdog = 0; |
| 21 |
while ((m = r.exec(str)) !== null && watchdog++ < 1000) { |
| 22 |
matches.push({ index: m.index, length: m[0].length, value: m[0], groups: Array.from(m).slice(1) }); |
| 23 |
if (m[0].length === 0) r.lastIndex++; |
| 24 |
} |
| 25 |
return matches; |
| 26 |
} |
| 27 |
|
| 28 |
function escapeHtml(str) { |
| 29 |
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); |
| 30 |
} |
| 31 |
|
| 32 |
function buildHighlightedHtml(str, matches, matchBg, matchBorder, accentColor) { |
| 33 |
if (!matches.length) return '<span>' + escapeHtml(str) + '</span>'; |
| 34 |
var html = ''; |
| 35 |
var cursor = 0; |
| 36 |
matches.forEach(function(m) { |
| 37 |
if (m.index > cursor) html += escapeHtml(str.slice(cursor, m.index)); |
| 38 |
html += '<mark class="bkbg-rt-match" style="background:' + matchBg + ';border-bottom-color:' + matchBorder + ';" title="Match: ' + escapeHtml(m.value) + '">' + escapeHtml(m.value) + '</mark>'; |
| 39 |
cursor = m.index + m.length; |
| 40 |
}); |
| 41 |
if (cursor < str.length) html += escapeHtml(str.slice(cursor)); |
| 42 |
return html; |
| 43 |
} |
| 44 |
|
| 45 |
var _typoKeys = { |
| 46 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 47 |
decoration:'text-decoration', transform:'text-transform', |
| 48 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', |
| 49 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', |
| 50 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', |
| 51 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' |
| 52 |
}; |
| 53 |
function typoCssVarsForEl(el, obj, prefix) { |
| 54 |
if (!obj || typeof obj !== 'object') return; |
| 55 |
Object.keys(_typoKeys).forEach(function(k) { |
| 56 |
var v = obj[k]; |
| 57 |
if (v === undefined || v === '' || v === null) return; |
| 58 |
if (k === 'sizeDesktop' || k === 'sizeTablet' || k === 'sizeMobile') v = v + (obj.sizeUnit || 'px'); |
| 59 |
else if (k === 'lineHeightDesktop' || k === 'lineHeightTablet' || k === 'lineHeightMobile') v = v + (obj.lineHeightUnit || ''); |
| 60 |
else if (k === 'letterSpacingDesktop' || k === 'letterSpacingTablet' || k === 'letterSpacingMobile') v = v + (obj.letterSpacingUnit || 'px'); |
| 61 |
else if (k === 'wordSpacingDesktop' || k === 'wordSpacingTablet' || k === 'wordSpacingMobile') v = v + (obj.wordSpacingUnit || 'px'); |
| 62 |
el.style.setProperty(prefix + _typoKeys[k], String(v)); |
| 63 |
}); |
| 64 |
} |
| 65 |
|
| 66 |
document.querySelectorAll('.bkbg-rt-app').forEach(function (root) { |
| 67 |
var opts = {}; |
| 68 |
try { opts = JSON.parse(root.getAttribute('data-opts') || '{}'); } catch(e) {} |
| 69 |
|
| 70 |
var accentColor = opts.accentColor || '#f59e0b'; |
| 71 |
var matchBg = opts.matchBg || '#fef3c7'; |
| 72 |
var matchBorder = opts.matchBorder || '#f59e0b'; |
| 73 |
var editorBg = opts.editorBg || '#1e1e2e'; |
| 74 |
var editorText = opts.editorText || '#cdd6f4'; |
| 75 |
var cardBg = opts.cardBg || '#ffffff'; |
| 76 |
var sectionBg = opts.sectionBg || '#f9fafb'; |
| 77 |
var titleColor = opts.titleColor || '#111827'; |
| 78 |
var subtitleColor = opts.subtitleColor || '#6b7280'; |
| 79 |
var labelColor = opts.labelColor || '#374151'; |
| 80 |
var errorColor = opts.errorColor || '#dc2626'; |
| 81 |
var showGroups = opts.showGroups !== false; |
| 82 |
var showCheatsheet = opts.showCheatsheet === true; |
| 83 |
|
| 84 |
var state = { |
| 85 |
pattern: opts.defaultPattern || '', |
| 86 |
flags: opts.defaultFlags || 'gi', |
| 87 |
test: opts.defaultTestStr || '' |
| 88 |
}; |
| 89 |
|
| 90 |
var FLAG_INFO = { g: 'global', i: 'ignore case', m: 'multiline', s: 'dotAll' }; |
| 91 |
|
| 92 |
function render() { |
| 93 |
var result = tryRegex(state.pattern, state.flags); |
| 94 |
var matches = result.re ? getMatches(result.re, state.test) : []; |
| 95 |
var hasError = !!result.error; |
| 96 |
var flagsArr = ['g','i','m','s']; |
| 97 |
|
| 98 |
root.innerHTML = |
| 99 |
'<div class="bkbg-rt-card" style="background:' + cardBg + ';max-width:' + (opts.contentMaxWidth||780) + 'px;">' + |
| 100 |
'<h2 class="bkbg-rt-title" style="color:' + titleColor + ';">' + (opts.title || 'Regex Tester') + '</h2>' + |
| 101 |
'<p class="bkbg-rt-subtitle" style="color:' + subtitleColor + ';">' + (opts.subtitle || 'Test regular expressions with live match highlighting') + '</p>' + |
| 102 |
|
| 103 |
'<div class="bkbg-rt-pattern-row">' + |
| 104 |
'<div class="bkbg-rt-pattern-wrap" style="background:' + editorBg + ';">' + |
| 105 |
'<span class="bkbg-rt-slash" style="color:' + accentColor + ';">/</span>' + |
| 106 |
'<input type="text" class="bkbg-rt-pattern-input" id="rt-pattern" value="' + escapeHtml(state.pattern) + '" placeholder="Enter pattern…" style="color:' + editorText + ';background:transparent;" spellcheck="false">' + |
| 107 |
'<span class="bkbg-rt-slash" style="color:' + accentColor + ';">/</span>' + |
| 108 |
'<input type="text" class="bkbg-rt-flags-input" id="rt-flags" value="' + escapeHtml(state.flags) + '" placeholder="gi" maxlength="6" style="color:' + accentColor + ';background:transparent;" spellcheck="false">' + |
| 109 |
'</div>' + |
| 110 |
(hasError ? '<div class="bkbg-rt-error" style="color:' + errorColor + ';">⚠ ' + escapeHtml(result.error) + '</div>' : '') + |
| 111 |
'<div class="bkbg-rt-flags-row">' + |
| 112 |
flagsArr.map(function(f) { |
| 113 |
var active = state.flags.includes(f); |
| 114 |
return '<button class="bkbg-rt-flag-btn" data-flag="' + f + '" style="background:' + (active ? accentColor : '#f3f4f6') + ';color:' + (active ? '#fff' : labelColor) + ';"><strong>' + f + '</strong><span>' + FLAG_INFO[f] + '</span></button>'; |
| 115 |
}).join('') + |
| 116 |
'</div>' + |
| 117 |
'</div>' + |
| 118 |
|
| 119 |
'<div class="bkbg-rt-section">' + |
| 120 |
'<label class="bkbg-rt-label" style="color:' + labelColor + ';">Test String</label>' + |
| 121 |
'<textarea class="bkbg-rt-textarea" id="rt-test" style="background:' + sectionBg + ';color:' + labelColor + ';min-height:' + (opts.editorHeight||160) + 'px;" spellcheck="false">' + escapeHtml(state.test) + '</textarea>' + |
| 122 |
'</div>' + |
| 123 |
|
| 124 |
'<div class="bkbg-rt-stats" style="background:' + sectionBg + ';color:' + labelColor + ';">' + |
| 125 |
(hasError |
| 126 |
? '<span style="color:' + errorColor + ';">⚠ Invalid regex pattern</span>' |
| 127 |
: '<span>Matches: <strong style="color:' + accentColor + ';">' + matches.length + '</strong></span>' + |
| 128 |
(matches.length > 0 ? '<span>Groups per match: <strong>' + Math.max(0, matches[0].groups.length) + '</strong></span>' : '') + |
| 129 |
'<span>Test length: <strong>' + state.test.length + '</strong> chars</span>' |
| 130 |
) + |
| 131 |
'</div>' + |
| 132 |
|
| 133 |
(!hasError && state.test ? |
| 134 |
'<div class="bkbg-rt-section">' + |
| 135 |
'<label class="bkbg-rt-label" style="color:' + labelColor + ';">Highlighted Output</label>' + |
| 136 |
'<div class="bkbg-rt-output" style="background:' + sectionBg + ';font-size:' + (opts.editorFontSize||14) + 'px;">' + |
| 137 |
buildHighlightedHtml(state.test, matches, matchBg, matchBorder, accentColor) + |
| 138 |
'</div>' + |
| 139 |
'</div>' : '') + |
| 140 |
|
| 141 |
(showGroups && matches.length > 0 ? |
| 142 |
'<div class="bkbg-rt-matches">' + |
| 143 |
'<div class="bkbg-rt-matches-title" style="color:' + labelColor + ';">Matches (' + matches.length + ')</div>' + |
| 144 |
matches.slice(0, 30).map(function(m, i) { |
| 145 |
return '<div class="bkbg-rt-match-badge" style="border-color:' + accentColor + '55;background:' + accentColor + '0e;">' + |
| 146 |
'<span class="bkbg-rt-match-idx" style="color:' + accentColor + ';">#' + (i+1) + '</span>' + |
| 147 |
'<span class="bkbg-rt-match-val" style="color:' + labelColor + ';">' + escapeHtml(m.value) + '</span>' + |
| 148 |
(m.groups.some(function(g){ return g !== undefined; }) |
| 149 |
? '<span class="bkbg-rt-match-groups">Groups: ' + m.groups.map(function(g,gi){ return 'G'+(gi+1)+': '+(g||'—'); }).join(' | ') + '</span>' |
| 150 |
: '') + |
| 151 |
'</div>'; |
| 152 |
}).join('') + |
| 153 |
(matches.length > 30 ? '<div class="bkbg-rt-more" style="color:#9ca3af;">+ ' + (matches.length-30) + ' more matches</div>' : '') + |
| 154 |
'</div>' : '') + |
| 155 |
'</div>'; |
| 156 |
|
| 157 |
var card = root.querySelector('.bkbg-rt-card'); |
| 158 |
if (card) { |
| 159 |
typoCssVarsForEl(card, opts.titleTypo || {}, '--bkbgrt-tt-'); |
| 160 |
typoCssVarsForEl(card, opts.editorTypo || {}, '--bkbgrt-et-'); |
| 161 |
} |
| 162 |
|
| 163 |
bindEvents(); |
| 164 |
} |
| 165 |
|
| 166 |
function bindEvents() { |
| 167 |
var patInput = root.querySelector('#rt-pattern'); |
| 168 |
var flagInput = root.querySelector('#rt-flags'); |
| 169 |
var testArea = root.querySelector('#rt-test'); |
| 170 |
|
| 171 |
function update() { render(); } |
| 172 |
|
| 173 |
if (patInput) patInput.addEventListener('input', function() { state.pattern = this.value; update(); }); |
| 174 |
if (flagInput) flagInput.addEventListener('input', function() { |
| 175 |
state.flags = this.value.replace(/[^gimsuy]/g,''); update(); |
| 176 |
}); |
| 177 |
if (testArea) testArea.addEventListener('input', function() { state.test = this.value; update(); }); |
| 178 |
|
| 179 |
root.querySelectorAll('.bkbg-rt-flag-btn[data-flag]').forEach(function(btn) { |
| 180 |
btn.addEventListener('click', function() { |
| 181 |
var f = this.getAttribute('data-flag'); |
| 182 |
if (state.flags.includes(f)) { |
| 183 |
state.flags = state.flags.replace(f, ''); |
| 184 |
} else { |
| 185 |
state.flags += f; |
| 186 |
} |
| 187 |
update(); |
| 188 |
}); |
| 189 |
}); |
| 190 |
} |
| 191 |
|
| 192 |
render(); |
| 193 |
}); |
| 194 |
|
| 195 |
})(); |
| 196 |
|