diff options
Diffstat (limited to '_posts/2026-10-17-regex.html')
| -rw-r--r-- | _posts/2026-10-17-regex.html | 512 |
1 files changed, 512 insertions, 0 deletions
diff --git a/_posts/2026-10-17-regex.html b/_posts/2026-10-17-regex.html new file mode 100644 index 0000000..addb3de --- /dev/null +++ b/_posts/2026-10-17-regex.html @@ -0,0 +1,512 @@ +--- +layout: post +--- + +<label> + <input type="checkbox" id="fullscreen"> + Fullscreen +</label> + +<script> + const body = document.body; + const checkbox = document.getElementById("fullscreen"); + const properties = ["margin", "max-width", "padding"]; + + // Save each property's original inline value and priority. + const originalStyles = Object.fromEntries( + properties.map(property => [ + property, + { + value: body.style.getPropertyValue(property), + priority: body.style.getPropertyPriority(property) + } + ]) + ); + + checkbox.addEventListener("change", () => { + if (checkbox.checked) { + body.style.setProperty("margin", "0"); + body.style.setProperty("max-width", "none"); + body.style.setProperty("padding", "0"); + } else { + for (const property of properties) { + const { value, priority } = originalStyles[property]; + + if (value) { + body.style.setProperty(property, value, priority); + } else { + body.style.removeProperty(property); + } + } + } + }); +</script> + + +<div id="loading"> + <div class="spinner"></div> + <div>Loading editor…</div> +</div> + +<style> + :root { + --bg-color: #ffffff; + --border-color: #d0d7de; + --text-color: #24292f; + --tab-bg: #f6f8fa; + --tab-active-bg: #ffffff; + } + + #loading { + position: absolute; + inset: 0; + z-index: 10; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + background: var(--bg-color); + font-size: 14px; + } + #loading .spinner { + width: 28px; + height: 28px; + border: 3px solid var(--border-color); + border-top-color: #0969da; + border-radius: 50%; + animation: spin 0.8s linear infinite; + } + @keyframes spin { to { transform: rotate(360deg); } } + + /* Match highlights (bottom editor) */ + .cm-regex-match-0 { background-color: rgba(9, 105, 218, 0.15); border-bottom: 2px solid #0969da; border-radius: 2px; } + .cm-regex-match-1 { background-color: rgba(45, 164, 78, 0.15); border-bottom: 2px solid #2da44e; border-radius: 2px; } + .cm-regex-match-2 { background-color: rgba(207, 34, 46, 0.15); border-bottom: 2px solid #cf222e; border-radius: 2px; } + .cm-regex-match-3 { background-color: rgba(154, 103, 0, 0.15); border-bottom: 2px solid #9a6700; border-radius: 2px; } + .cm-regex-match-4 { background-color: rgba(130, 80, 223, 0.15); border-bottom: 2px solid #8250df; border-radius: 2px; } + + /* Line fringe highlights (top editor) */ + .cm-regex-line-0 { border-left: 4px solid #0969da; background-color: rgba(9, 105, 218, 0.05); } + .cm-regex-line-1 { border-left: 4px solid #2da44e; background-color: rgba(45, 164, 78, 0.05); } + .cm-regex-line-2 { border-left: 4px solid #cf222e; background-color: rgba(207, 34, 46, 0.05); } + .cm-regex-line-3 { border-left: 4px solid #9a6700; background-color: rgba(154, 103, 0, 0.05); } + .cm-regex-line-4 { border-left: 4px solid #8250df; background-color: rgba(130, 80, 223, 0.05); } + + .tab-bar-container { + display: flex; + align-items: center; + background: var(--border-color); + padding: 1px 1px 0 1px; + overflow: hidden; + } + .tab-bar { + display: flex; + gap: 1px; + overflow-x: auto; + flex: 1; + } + .tab-btn { + background: var(--tab-bg); + color: var(--text-color); + border: none; + padding: 6px 10px 6px 14px; + cursor: pointer; + font-size: 13px; + display: flex; + align-items: center; + gap: 8px; + white-space: nowrap; + user-select: none; + } + .tab-btn input { + font: inherit; + padding: 0 2px; + border: 1px solid #0969da; + outline: none; + background: #fff; + } + .tab-btn.active { + background: var(--tab-active-bg); + border-bottom: 2px solid #0969da; + font-weight: 500; + } + .tab-close { + background: transparent; + border: none; + color: #8c959f; + cursor: pointer; + font-size: 14px; + padding: 0 4px; + border-radius: 4px; + } + .tab-close:hover { + background: rgba(0, 0, 0, 0.1); + color: var(--text-color); + } + .tab-add-btn { + background: var(--tab-bg); + color: var(--text-color); + border: none; + cursor: pointer; + font-size: 16px; + font-weight: bold; + } + .tab-add-btn:hover { + background: var(--tab-active-bg); + } + + #regex-editor { + margin-bottom: 1rem; + border: 1px solid var(--border-color); + overflow: hidden; + background: #ffffff; + } + + #cm6-editor { + flex: 1; + display: flex; + flex-direction: column; + border: 1px solid var(--border-color); + overflow: hidden; + background: #ffffff; + } + + #regex-editor .cm-editor, + #cm6-editor .cm-editor { + height: 100%; + } + + #regex-editor .cm-scroller, + #cm6-editor .cm-scroller { + overflow: auto; + } + + .cm-editor.cm-focused, + .cm-scroller:focus, + .cm-content:focus { + outline: none !important; + } +</style> + +<div id="regex-editor"></div> + +<div class="tab-bar-container"> + <div class="tab-bar" id="tab-bar"> + <!-- Tabs will be rendered dynamically --> + </div> + <button class="tab-add-btn" id="add-tab-btn" title="Add New Tab">+</button> +</div> +<div id="cm6-editor"></div> + +<script type="module"> + import { EditorState, StateField, StateEffect, RangeSetBuilder } from 'https://esm.sh/@codemirror/state@6.7.6'; + import { EditorView, Decoration, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, highlightActiveLine } from 'https://esm.sh/@codemirror/view@6.43.14'; + import { defaultKeymap, history, historyKeymap } from 'https://esm.sh/@codemirror/commands@6.11.1'; + import { syntaxHighlighting, defaultHighlightStyle, bracketMatching } from 'https://esm.sh/@codemirror/language@6.13.1'; + + // Top Editor Line Fringe Decorations + const topEditorLineDecos = [ + Decoration.line({ class: 'cm-regex-line-0' }), + Decoration.line({ class: 'cm-regex-line-1' }), + Decoration.line({ class: 'cm-regex-line-2' }), + Decoration.line({ class: 'cm-regex-line-3' }), + Decoration.line({ class: 'cm-regex-line-4' }) + ]; + + const topEditorField = StateField.define({ + create: state => buildTopDecorations(state), + update: (decorations, tr) => tr.docChanged ? buildTopDecorations(tr.state) : decorations, + provide: f => EditorView.decorations.from(f) + }); + + function buildTopDecorations(state) { + const builder = new RangeSetBuilder(); + for (let i = 1; i <= state.doc.lines; i++) { + const line = state.doc.line(i); + builder.add(line.from, line.from, topEditorLineDecos[(i - 1) % topEditorLineDecos.length]); + } + return builder.finish(); + } + + + + // Load saved regex patterns or fallback to default + const savedRegex = localStorage.getItem('regex_patterns') || "/([a-zA-Z0-9._-]+)@([a-zA-Z0-9._-]+)\\.([a-zA-Z]{2,5})/\n/([0-9]{4}-[0-9]{2}-[0-9]{2})/"; + + // Load saved tabs/documents or fallback to defaults + let documents = JSON.parse(localStorage.getItem('regex_documents')) || [ + `Welcome to CodeMirror 6 Regex Visualizer! + + Test email addresses: + - Support Team: support@example.com + - Main Info: info@domain.co.uk + - Developer: dev.team@service.org + - Invalid attempt: test@@domain..com + + System parameters: + - User ID: 1042 | Session Token: abc123XYZ + - Created on: 2026-10-17`, + + `function testAuth() { + const token = "Bearer eyJhbGciOiJIUzI1NiIsIn"; + let userId = 9921; + // TODO: sanitize input + const email = "admin@system.local"; + } + const date = "2026-10-18";`, + + `[2026-10-18 10:15:30] INFO: User login success (uid=402, ip=192.168.1.50) + [2026-10-18 10:16:02] ERROR: Invalid token provided (token=xyz_9988_abc) + [2026-10-18 10:17:11] WARN: Rate limit approaching for service@api.net` + ]; + + let tabTitles = JSON.parse(localStorage.getItem('regex_tab_titles')) || ["Emails & IDs", "Code Snippet", "Server Logs"]; + let activeTabIndex = parseInt(localStorage.getItem('regex_active_tab')) || 0; + if (activeTabIndex >= documents.length) activeTabIndex = 0; + + function saveToStorage() { + // Keep current active tab text up to date in the array before saving + documents[activeTabIndex] = mainView.state.doc.toString(); + + localStorage.setItem('regex_patterns', regexView.state.doc.toString()); + localStorage.setItem('regex_documents', JSON.stringify(documents)); + localStorage.setItem('regex_tab_titles', JSON.stringify(tabTitles)); + localStorage.setItem('regex_active_tab', activeTabIndex.toString()); + } + + const initialRegex = "/([a-zA-Z0-9.\-_]+)@([a-zA-Z0-9.\-_]+)\\.([a-zA-Z]{2,5})/\n/([0-9]{4}-[0-9]{2}-[0-9]{2})/"; + + const regexView = new EditorView({ + state: EditorState.create({ + doc: savedRegex, + extensions: [ + lineNumbers(), + drawSelection(), + keymap.of(defaultKeymap), + topEditorField, + EditorView.updateListener.of(update => { + if (update.docChanged) { + dispatchRegexUpdate(); + saveToStorage(); + } + }) + ] + }), + parent: document.getElementById('regex-editor') + }); + + const tabBarEl = document.getElementById('tab-bar'); + + function renderTabs() { + tabBarEl.innerHTML = ''; + documents.forEach((_, index) => { + const tab = document.createElement('div'); + tab.className = 'tab-btn' + (index === activeTabIndex ? ' active' : ''); + tab.setAttribute('role', 'tab'); + + const label = document.createElement('span'); + label.textContent = tabTitles[index] || `Tab ${index + 1}`; + label.title = 'Double-click to rename'; + label.addEventListener('dblclick', () => renameTab(index, label)); + tab.appendChild(label); + + if (documents.length > 1) { + const closeBtn = document.createElement('span'); + closeBtn.className = 'tab-close'; + closeBtn.setAttribute('role', 'button'); + closeBtn.title = 'Close Tab'; + closeBtn.textContent = '×'; + closeBtn.addEventListener('click', e => { e.stopPropagation(); closeTab(index); }); + tab.appendChild(closeBtn); + } + + tab.addEventListener('click', () => switchTab(index)); + tabBarEl.appendChild(tab); + }); + } + + function renameTab(index, label) { + const input = document.createElement('input'); + input.value = tabTitles[index] || ''; + input.size = Math.max(6, input.value.length); + label.replaceWith(input); + input.focus(); + input.select(); + + let done = false; + const finish = save => { + if (done) return; // blur also fires when the input is removed + done = true; + const name = input.value.trim(); + if (save && name) tabTitles[index] = name; // empty name keeps the old one + renderTabs(); + saveToStorage(); + }; + + input.addEventListener('keydown', e => { + e.stopPropagation(); + if (e.key === 'Enter') finish(true); + if (e.key === 'Escape') finish(false); + }); + input.addEventListener('blur', () => finish(true)); + input.addEventListener('click', e => e.stopPropagation()); // don't trigger switchTab + } + + function loadActive() { + renderTabs(); + mainView.setState(states[activeTabIndex]); + dispatchRegexUpdate(); // a stashed state may hold stale regex queries + saveToStorage(); // must run after setState so it saves the new tab's text + } + + function switchTab(index) { + if (index === activeTabIndex) return; + states[activeTabIndex] = mainView.state; // stash undo history + cursor + activeTabIndex = index; + loadActive(); + } + + function closeTab(index) { + const tabName = tabTitles[index] || `Tab ${index + 1}`; + if (!window.confirm(`Are you sure you want to close "${tabName}"?`)) return; + + states[activeTabIndex] = mainView.state; + [documents, tabTitles, states].forEach(a => a.splice(index, 1)); + + if (index < activeTabIndex) activeTabIndex--; // also fixes the wrong-tab bug + else activeTabIndex = Math.min(activeTabIndex, documents.length - 1); + loadActive(); + } + + document.getElementById('add-tab-btn').addEventListener('click', () => { + const text = "// Type or paste text here...\n"; + states[activeTabIndex] = mainView.state; + documents.push(text); + tabTitles.push(`Dataset ${documents.length}`); + states.push(makeState(text)); + activeTabIndex = documents.length - 1; + loadActive(); + }); + + // Initial Render Call + renderTabs(); + + const updateRegexEffect = StateEffect.define(); + + const regexMatchDecos = [ + Decoration.mark({ class: 'cm-regex-match-0' }), + Decoration.mark({ class: 'cm-regex-match-1' }), + Decoration.mark({ class: 'cm-regex-match-2' }), + Decoration.mark({ class: 'cm-regex-match-3' }), + Decoration.mark({ class: 'cm-regex-match-4' }) + ]; + + const regexHighlightField = StateField.define({ + create: () => ({ decorations: Decoration.none, query: [] }), + update(value, tr) { + let currentQueries = value.query; + for (let effect of tr.effects) { + if (effect.is(updateRegexEffect)) currentQueries = effect.value; + } + if (!tr.docChanged && tr.effects.length === 0) return value; + + const docText = tr.state.doc.toString(); + const ranges = []; + + currentQueries.forEach(q => { + if (!q.pattern) return; + let activeRegex; + try { + activeRegex = new RegExp(q.pattern, q.flags); + } catch (err) { + return; + } + + let match; + const deco = regexMatchDecos[q.lineIndex % regexMatchDecos.length]; + + if (q.flags.includes('g')) { + while ((match = activeRegex.exec(docText)) !== null) { + const fullStart = match.index; + const fullEnd = fullStart + match[0].length; + if (fullStart !== fullEnd) { + ranges.push({ from: fullStart, to: fullEnd, deco }); + } + if (match.index === activeRegex.lastIndex) { + activeRegex.lastIndex++; + } + } + } else { + match = activeRegex.exec(docText); + if (match) { + const fullStart = match.index; + const fullEnd = fullStart + match[0].length; + if (fullStart !== fullEnd) { + ranges.push({ from: fullStart, to: fullEnd, deco }); + } + } + } + }); + + ranges.sort((a, b) => a.from - b.from || a.to - b.to); + const builder = new RangeSetBuilder(); + ranges.forEach(r => builder.add(r.from, r.to, r.deco)); + + return { decorations: builder.finish(), query: currentQueries }; + }, + provide: f => EditorView.decorations.from(f, val => val.decorations) + }); + + const mainExtensions = [ + lineNumbers(), + highlightActiveLineGutter(), + highlightSpecialChars(), + history(), + drawSelection(), + highlightActiveLine(), + bracketMatching(), + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + keymap.of([...defaultKeymap, ...historyKeymap]), + regexHighlightField, + EditorView.updateListener.of(u => { if (u.docChanged) saveToStorage(); }) + ]; + + const makeState = doc => EditorState.create({ doc, extensions: mainExtensions }); + const states = documents.map(makeState); // one EditorState per tab, parallel to documents/tabTitles + + const mainView = new EditorView({ + state: states[activeTabIndex], + parent: document.getElementById('cm6-editor') + }); + + function dispatchRegexUpdate() { + const lines = regexView.state.doc.toString().split('\n'); + const queries = []; + + lines.forEach((lineText, index) => { + const trimmed = lineText.trim(); + if (!trimmed) return; + + let pattern = trimmed; + let flags = 'g'; + + const lastSlashIndex = trimmed.lastIndexOf('/'); + if (trimmed.startsWith('/') && lastSlashIndex > 0) { + pattern = trimmed.substring(1, lastSlashIndex); + flags = trimmed.substring(lastSlashIndex + 1) || 'g'; + } + + queries.push({ pattern, flags, lineIndex: index }); + }); + + mainView.dispatch({ + effects: updateRegexEffect.of(queries) + }); + } + + // Initial setup call + dispatchRegexUpdate(); + + document.getElementById('loading').remove(); +</script> |