summaryrefslogtreecommitdiff
path: root/_posts/2026-10-17-regex.html
diff options
context:
space:
mode:
authorHizbu Azka <hanifan.azka@gmail.com>2026-10-23 10:45:25 +0700
committerHizbu Azka <hanifan.azka@gmail.com>2026-10-23 10:46:41 +0700
commit272b2b94a268e1e342dab2daae4031f4eabdbb8e (patch)
tree8e5c49b6e528e5a1a7aa4190a21fbffeebdde165 /_posts/2026-10-17-regex.html
parent2bd366b984bb0ac3ac98691a51f837731af67b2a (diff)
run prettierHEADmain
Diffstat (limited to '_posts/2026-10-17-regex.html')
-rw-r--r--_posts/2026-10-17-regex.html746
1 files changed, 410 insertions, 336 deletions
diff --git a/_posts/2026-10-17-regex.html b/_posts/2026-10-17-regex.html
index b03779a..bb0ceb4 100644
--- a/_posts/2026-10-17-regex.html
+++ b/_posts/2026-10-17-regex.html
@@ -3,7 +3,7 @@ layout: post
---
<label>
- <input type="checkbox" id="fullscreen">
+ <input type="checkbox" id="fullscreen" />
Fullscreen
</label>
@@ -11,19 +11,18 @@ layout: post
const body = document.body;
const checkbox = document.getElementById("fullscreen");
checkbox.addEventListener("change", () => {
- if (checkbox.checked) {
- body.style.setProperty("margin", "0");
- body.style.setProperty("max-width", "none");
- body.style.setProperty("padding", "0");
- } else {
- body.style.removeProperty("margin");
- body.style.removeProperty("max-width");
- body.style.removeProperty("padding");
- }
+ if (checkbox.checked) {
+ body.style.setProperty("margin", "0");
+ body.style.setProperty("max-width", "none");
+ body.style.setProperty("padding", "0");
+ } else {
+ body.style.removeProperty("margin");
+ body.style.removeProperty("max-width");
+ body.style.removeProperty("padding");
+ }
});
</script>
-
<div id="loading">
<div class="spinner"></div>
<div>Loading editor…</div>
@@ -31,155 +30,194 @@ layout: post
<style>
:root {
- --border-color: #d0d7de;
- --tab-bg: #f6f8fa;
- --tab-active-bg: #ffffff;
- --editor-bg: #ffffff;
- --rename-bg: #ffffff;
- --rename-fg: #000000;
+ --border-color: #d0d7de;
+ --tab-bg: #f6f8fa;
+ --tab-active-bg: #ffffff;
+ --editor-bg: #ffffff;
+ --rename-bg: #ffffff;
+ --rename-fg: #000000;
}
@media (prefers-color-scheme: dark) {
- :root {
- --border-color: #2f2821;
- --tab-bg: #090705;
- --tab-active-bg: #000000;
- --editor-bg: #0d1117;
- --rename-bg: #0d1117;
- --rename-fg: #ffffff;
- }
+ :root {
+ --border-color: #2f2821;
+ --tab-bg: #090705;
+ --tab-active-bg: #000000;
+ --editor-bg: #0d1117;
+ --rename-bg: #0d1117;
+ --rename-fg: #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;
+ 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;
+ 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);
+ }
}
- @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; }
+ .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); }
+ .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;
+ 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;
+ 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;
+ 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: var(--rename-bg);
- color: var(--rename-fg);
+ font: inherit;
+ padding: 0 2px;
+ border: 1px solid #0969da;
+ outline: none;
+ background: var(--rename-bg);
+ color: var(--rename-fg);
}
.tab-btn.active {
- background: var(--tab-active-bg);
- border-bottom: 2px solid #0969da;
- font-weight: 500;
+ 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;
+ background: transparent;
+ border: none;
+ color: #8c959f;
+ cursor: pointer;
+ font-size: 14px;
+ padding: 0 4px;
+ border-radius: 4px;
}
.tab-close:hover {
- background: rgba(128, 128, 128, 0.25);
- color: var(--text-color);
+ background: rgba(128, 128, 128, 0.25);
+ 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;
+ 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);
+ background: var(--tab-active-bg);
}
#regex-editor {
- margin-bottom: 1rem;
- border: 1px solid var(--border-color);
- overflow: hidden;
+ margin-bottom: 1rem;
+ border: 1px solid var(--border-color);
+ overflow: hidden;
background: var(--editor-bg);
}
#cm6-editor {
- flex: 1;
- display: flex;
- flex-direction: column;
- border: 1px solid var(--border-color);
- overflow: hidden;
- background: var(--editor-bg);
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--border-color);
+ overflow: hidden;
+ background: var(--editor-bg);
}
#regex-editor .cm-editor,
#cm6-editor .cm-editor {
- height: 100%;
+ height: 100%;
}
#regex-editor .cm-scroller,
#cm6-editor .cm-scroller {
- overflow: auto;
+ overflow: auto;
}
- .cm-editor.cm-focused,
- .cm-scroller:focus,
+ .cm-editor.cm-focused,
+ .cm-scroller:focus,
.cm-content:focus {
- outline: none !important;
+ outline: none !important;
}
</style>
@@ -196,50 +234,74 @@ layout: post
{% include cm-importmap.html %}
<script type="module">
- import { EditorState, StateField, StateEffect, RangeSetBuilder, Compartment } from '@codemirror/state';
- import { EditorView, Decoration, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, highlightActiveLine } from '@codemirror/view';
- import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
- import { syntaxHighlighting, defaultHighlightStyle, bracketMatching } from '@codemirror/language';
+ import {
+ EditorState,
+ StateField,
+ StateEffect,
+ RangeSetBuilder,
+ Compartment,
+ } from "@codemirror/state";
+ import {
+ EditorView,
+ Decoration,
+ keymap,
+ lineNumbers,
+ highlightActiveLineGutter,
+ highlightSpecialChars,
+ drawSelection,
+ highlightActiveLine,
+ } from "@codemirror/view";
+ import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
+ import {
+ syntaxHighlighting,
+ defaultHighlightStyle,
+ bracketMatching,
+ } from "@codemirror/language";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
- const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
- const themeFor = dark => dark ? githubDark : githubLight;
+ const darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
+ const themeFor = (dark) => (dark ? githubDark : githubLight);
const regexTheme = new Compartment();
- const mainTheme = new Compartment();
+ const mainTheme = new Compartment();
// 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' })
+ 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)
+ 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();
- }
-
-
+ 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 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!
+ // 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
@@ -251,7 +313,7 @@ layout: post
- User ID: 1042 | Session Token: abc123XYZ
- Created on: 2026-10-17`,
- `function testAuth() {
+ `function testAuth() {
const token = "Bearer eyJhbGciOiJIUzI1NiIsIn";
let userId = 9921;
// TODO: sanitize input
@@ -259,268 +321,280 @@ layout: post
}
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: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`
- ];
+ [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;
+ 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() {
+ 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})/";
+ 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 regexView = new EditorView({
+ 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),
- regexTheme.of(themeFor(darkQuery.matches)),
- topEditorField,
- EditorView.updateListener.of(update => {
- if (update.docChanged) {
- dispatchRegexUpdate();
- saveToStorage();
- }
- })
- ]
+ doc: savedRegex,
+ extensions: [
+ lineNumbers(),
+ drawSelection(),
+ keymap.of(defaultKeymap),
+ regexTheme.of(themeFor(darkQuery.matches)),
+ topEditorField,
+ EditorView.updateListener.of((update) => {
+ if (update.docChanged) {
+ dispatchRegexUpdate();
+ saveToStorage();
+ }
+ }),
+ ],
}),
- parent: document.getElementById('regex-editor')
- });
+ parent: document.getElementById("regex-editor"),
+ });
- const tabBarEl = document.getElementById('tab-bar');
+ const tabBarEl = document.getElementById("tab-bar");
- function renderTabs() {
- tabBarEl.innerHTML = '';
+ 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);
- }
+ 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);
+ tab.addEventListener("click", () => switchTab(index));
+ tabBarEl.appendChild(tab);
});
- }
+ }
- function renameTab(index, label) {
- const input = document.createElement('input');
- input.value = tabTitles[index] || '';
+ 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();
+ 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("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
- }
+ input.addEventListener("blur", () => finish(true));
+ input.addEventListener("click", (e) => e.stopPropagation()); // don't trigger switchTab
+ }
function applyTheme() {
- const t = themeFor(darkQuery.matches);
- regexView.dispatch({ effects: regexTheme.reconfigure(t) });
- mainView.dispatch({ effects: mainTheme.reconfigure(t) });
+ const t = themeFor(darkQuery.matches);
+ regexView.dispatch({ effects: regexTheme.reconfigure(t) });
+ mainView.dispatch({ effects: mainTheme.reconfigure(t) });
}
- darkQuery.addEventListener('change', applyTheme);
+ darkQuery.addEventListener("change", applyTheme);
- function loadActive() {
+ function loadActive() {
renderTabs();
mainView.setState(states[activeTabIndex]);
- applyTheme();
- dispatchRegexUpdate(); // a stashed state may hold stale regex queries
- saveToStorage(); // must run after setState so it saves the new tab's text
- }
+ applyTheme();
+ 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) {
+ function switchTab(index) {
if (index === activeTabIndex) return;
- states[activeTabIndex] = mainView.state; // stash undo history + cursor
+ states[activeTabIndex] = mainView.state; // stash undo history + cursor
activeTabIndex = index;
loadActive();
- }
+ }
- function closeTab(index) {
+ 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();
- });
+ [documents, tabTitles, states].forEach((a) => a.splice(index, 1));
- // 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) {
+ 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 (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 });
- }
- }
- }
+ 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));
+ 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)
- });
+ },
+ provide: (f) => EditorView.decorations.from(f, (val) => val.decorations),
+ });
- const mainExtensions = [
- lineNumbers(),
- highlightActiveLineGutter(),
- highlightSpecialChars(),
- history(),
- drawSelection(),
- highlightActiveLine(),
- bracketMatching(),
- mainTheme.of(themeFor(darkQuery.matches)),
- // 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')
- });
+ const mainExtensions = [
+ lineNumbers(),
+ highlightActiveLineGutter(),
+ highlightSpecialChars(),
+ history(),
+ drawSelection(),
+ highlightActiveLine(),
+ bracketMatching(),
+ mainTheme.of(themeFor(darkQuery.matches)),
+ // 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
- function dispatchRegexUpdate() {
- const lines = regexView.state.doc.toString().split('\n');
- const queries = [];
+ const mainView = new EditorView({
+ state: states[activeTabIndex],
+ parent: document.getElementById("cm6-editor"),
+ });
- lines.forEach((lineText, index) => {
+ 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';
+ let flags = "g";
- const lastSlashIndex = trimmed.lastIndexOf('/');
- if (trimmed.startsWith('/') && lastSlashIndex > 0) {
- pattern = trimmed.substring(1, lastSlashIndex);
- flags = trimmed.substring(lastSlashIndex + 1) || '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)
- });
- }
+ mainView.dispatch({
+ effects: updateRegexEffect.of(queries),
+ });
+ }
- // Initial setup call
- dispatchRegexUpdate();
+ // Initial setup call
+ dispatchRegexUpdate();
- document.getElementById('loading').remove();
+ document.getElementById("loading").remove();
</script>