summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHizbu Azka <hanifan.azka@gmail.com>2026-10-18 01:34:02 +0700
committerHizbu Azka <hanifan.azka@gmail.com>2026-10-18 01:38:46 +0700
commitd92b1d07845b4128a3bc1262ac3674240b8101c3 (patch)
treef940e5d83453a783e4d8d617c5adc02b0bb6355a
parent99a16b4ef18c7b11f3430a1fa84ca0903fa25a5a (diff)
add post regexHEADmain
-rw-r--r--_config.yml1
-rw-r--r--_posts/2026-10-17-regex.html512
2 files changed, 513 insertions, 0 deletions
diff --git a/_config.yml b/_config.yml
index c8805a4..e440cc2 100644
--- a/_config.yml
+++ b/_config.yml
@@ -1,3 +1,4 @@
url: "https://hizbu.com"
baseurl: ""
title: "Hizbu Azka Personal Site"
+permalink: /:categories/:title/
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>