{"id":763,"date":"2026-03-11T18:08:11","date_gmt":"2026-03-11T18:08:11","guid":{"rendered":"https:\/\/primetoolhub.com\/?page_id=763"},"modified":"2026-07-11T05:11:40","modified_gmt":"2026-07-11T05:11:40","slug":"advanced-text-regex-tester-debugger","status":"publish","type":"page","link":"https:\/\/schoolict.net\/tools\/advanced-text-regex-tester-debugger\/","title":{"rendered":"Advanced Regex Tester | Test, Replace &amp; Extract Patterns Offline"},"content":{"rendered":"\n<div class=\"pth-hero-section\">\n    <div class=\"pth-hero-content\">\n        <h2>All-in-One Regex Studio Pro<\/h2>\n        <p>Test, debug, and learn Regular Expressions (Regex) securely offline. Features real-time text highlighting, capture group extraction, and a built-in cheat sheet.<\/p>\n\n        <div id=\"pth-toc-placeholder\"><\/div>\n    <\/div>\n\n<div class=\"pth-hero-image\">\n    <img data-no-lazy=\"1\"\n         src=\"https:\/\/schoolict.net\/tools\/wp-content\/uploads\/2026\/04\/free-offline-regex-tester-Tool-800x447.jpeg\"\n         width=\"800\"\n         height=\"447\"\n         alt=\"All-in-One Regex Studio Pro\"\n         fetchpriority=\"high\"\n         loading=\"eager\"\n         decoding=\"async\"\n         style=\"width:100%; height:auto; display:block;\">\n  <\/div>\n<\/div>\n\n\n\n\n<div class=\"wp-block-rank-math-toc-block\" id=\"rank-math-toc\"><h2>Table of Contents<\/h2><nav><ul><li><a href=\"#\ud83d\udd34-the-pattern-looks-right-but-matches-nothing\">\ud83d\udd34\u00a0The pattern looks right but matches nothing<\/a><\/li><li><a href=\"#\ud83d\udfe1-rewriting-text-with-backreferences-not-by-hand\">\ud83d\udfe1\u00a0Rewriting text with backreferences, not by hand<\/a><\/li><li><a href=\"#\ud83d\udfe2-harvesting-a-clean-list-out-of-a-messy-paste\">\ud83d\udfe2\u00a0Harvesting a clean list out of a messy paste<\/a><ul><li><a href=\"#\ud83d\udfe2-three-habits-that-prevent-most-regex-pain\">\ud83d\udfe2\u00a0Three habits that prevent most regex pain<\/a><\/li><\/ul><\/li><li><a href=\"#\ud83d\udd34-when-regex-is-the-wrong-tool-and-what-to-reach-for\">\ud83d\udd34\u00a0When regex is the wrong tool, and what to reach for<\/a><\/li><\/ul><\/nav><\/div>\n\n\n<!-- LSCACHE_DISABLE -->\r\n<div id=\"pth-regex-studio-wrapper\">\r\n    <style>\r\n        #pth-regex-studio-wrapper { box-sizing: border-box; font-family: 'Inter', system-ui, -apple-system, sans-serif; color: #1e293b; background-color: #ffffff; border-radius: 12px; overflow: hidden; border: 1px solid #e2e8f0; margin: 20px 0; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); }\r\n        #pth-regex-studio-wrapper * { box-sizing: border-box; min-width: 0; }\r\n\r\n        \/* Topbar *\/\r\n        #pth-regex-studio-wrapper .pth-topbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; padding: 20px 24px 0; }\r\n        #pth-regex-studio-wrapper .pth-title { margin: 0; font-size: 1.35rem; font-weight: 800; color: #0f172a; }\r\n        #pth-regex-studio-wrapper .pth-toolbar { display: flex; gap: 8px; flex-wrap: wrap; }\r\n        #pth-regex-studio-wrapper .pth-btn { border: none; padding: 8px 14px; font-size: 0.85rem; border-radius: 7px; cursor: pointer; font-weight: 700; font-family: inherit; display: inline-flex; align-items: center; gap: 6px; transition: background 0.2s; }\r\n        #pth-regex-studio-wrapper .pth-sample-btn { background: #eff6ff; color: #1d4ed8; }\r\n        #pth-regex-studio-wrapper .pth-sample-btn:hover { background: #dbeafe; }\r\n        #pth-regex-studio-wrapper .pth-fw-btn { background: #e2e8f0; color: #0f172a; }\r\n        #pth-regex-studio-wrapper .pth-fw-btn:hover { background: #cbd5e1; }\r\n        #pth-regex-studio-wrapper .pth-clear-btn { background: #fef2f2; color: #991b1b; }\r\n        #pth-regex-studio-wrapper .pth-clear-btn:hover { background: #fee2e2; }\r\n\r\n        #pth-regex-studio-wrapper .pth-meta-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; padding: 8px 24px 0; }\r\n        #pth-regex-studio-wrapper .pth-version-badge { background: #eff6ff; color: #1d4ed8; font-size: 0.72rem; font-weight: 800; padding: 4px 10px; border-radius: 20px; letter-spacing: 0.4px; }\r\n        #pth-regex-studio-wrapper .pth-status-offline { background: #ecfdf5; color: #047857; font-size: 0.72rem; font-weight: 800; padding: 4px 10px; border-radius: 20px; }\r\n\r\n        \/* Tabs *\/\r\n        #pth-regex-studio-wrapper .pth-tabbar { display: flex; gap: 4px; flex-wrap: wrap; padding: 16px 24px 0; border-bottom: 2px solid #e2e8f0; }\r\n        #pth-regex-studio-wrapper .pth-tab-btn { background: transparent; border: none; border-bottom: 3px solid transparent; margin-bottom: -2px; padding: 10px 15px; font-size: 0.9rem; font-weight: 700; color: #334155; cursor: pointer; font-family: inherit; border-radius: 6px 6px 0 0; display: inline-flex; align-items: center; gap: 6px; }\r\n        #pth-regex-studio-wrapper .pth-tab-btn:hover { background: #f1f5f9; color: #0f172a; }\r\n        #pth-regex-studio-wrapper .pth-tab-btn.active { color: #1d4ed8; border-bottom-color: #2563eb; background: #eff6ff; }\r\n\r\n        \/* Layout *\/\r\n        #pth-regex-studio-wrapper .pth-grid { display: grid; grid-template-columns: 1.8fr 1fr; gap: 0; }\r\n        #pth-regex-studio-wrapper .pth-main { padding: 24px; border-right: 1px solid #e2e8f0; }\r\n        #pth-regex-studio-wrapper .pth-side { padding: 24px; background: #f8fafc; }\r\n        #pth-regex-studio-wrapper .pth-grid.full-width { grid-template-columns: 1fr; }\r\n        #pth-regex-studio-wrapper .pth-grid.full-width .pth-side { display: none; }\r\n        #pth-regex-studio-wrapper .pth-grid.full-width .pth-main { border-right: none; }\r\n\r\n        \/* Panels *\/\r\n        #pth-regex-studio-wrapper .pth-panel { display: none; }\r\n        #pth-regex-studio-wrapper .pth-panel.active { display: block; }\r\n        #pth-regex-studio-wrapper .pth-panel-intro { font-size: 0.9rem; color: #334155; line-height: 1.6; margin: 0 0 18px; font-weight: 500; }\r\n\r\n        \/* Regex input *\/\r\n        #pth-regex-studio-wrapper .pth-field { margin-bottom: 18px; }\r\n        #pth-regex-studio-wrapper .pth-field > label { display: block; font-size: 0.9rem; font-weight: 700; margin-bottom: 8px; color: #334155; }\r\n        #pth-regex-studio-wrapper .pth-hint { font-size: 0.8rem; color: #334155; font-style: italic; margin: 0 0 6px; }\r\n        #pth-regex-studio-wrapper .pth-rx-wrap { display: flex; align-items: stretch; background: #f8fafc; border: 2px solid #cbd5e1; border-radius: 8px; overflow: hidden; }\r\n        #pth-regex-studio-wrapper .pth-rx-wrap:focus-within { border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.12); }\r\n        #pth-regex-studio-wrapper .pth-rx-slash { background: #e2e8f0; color: #334155; padding: 12px 15px; font-weight: 800; font-size: 1.1rem; display: flex; align-items: center; }\r\n        #pth-regex-studio-wrapper .pth-rx-pattern { flex-grow: 1; border: none; background: transparent; padding: 12px; font-family: 'Courier New', monospace; font-size: 1.05rem; color: #0f172a; font-weight: 600; outline: none; width: 100%; }\r\n        #pth-regex-studio-wrapper .pth-rx-flags { width: 84px; border: none; border-left: 1px solid #cbd5e1; background: transparent; padding: 12px; font-family: 'Courier New', monospace; font-size: 1.05rem; color: #1d4ed8; font-weight: 700; outline: none; letter-spacing: 2px; }\r\n\r\n        #pth-regex-studio-wrapper .pth-ta { width: 100%; min-height: 120px; padding: 14px; font-size: 0.92rem; font-family: 'Courier New', monospace; line-height: 1.6; color: #1e293b; background: #ffffff; border: 2px solid #cbd5e1; border-radius: 8px; resize: vertical; outline: none; }\r\n        #pth-regex-studio-wrapper .pth-ta:focus, #pth-regex-studio-wrapper .pth-inp:focus, #pth-regex-studio-wrapper .pth-sel:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.12); }\r\n        #pth-regex-studio-wrapper .pth-inp, #pth-regex-studio-wrapper .pth-sel { width: 100%; padding: 10px 12px; border: 2px solid #cbd5e1; border-radius: 8px; font-family: inherit; font-size: 0.9rem; color: #1e293b; background: #ffffff; }\r\n        #pth-regex-studio-wrapper .pth-inp.mono { font-family: 'Courier New', monospace; }\r\n        #pth-regex-studio-wrapper .pth-row { display: flex; gap: 12px; flex-wrap: wrap; }\r\n        #pth-regex-studio-wrapper .pth-row .pth-field { flex: 1 1 160px; margin-bottom: 0; }\r\n        #pth-regex-studio-wrapper .pth-check { display: inline-flex; align-items: center; gap: 6px; font-size: 0.85rem; font-weight: 600; color: #334155; margin-right: 16px; cursor: pointer; }\r\n\r\n        #pth-regex-studio-wrapper .pth-action-btn { background: #2563eb; color: #ffffff; border: none; padding: 11px 20px; font-size: 0.9rem; font-weight: 700; border-radius: 8px; cursor: pointer; font-family: inherit; }\r\n        #pth-regex-studio-wrapper .pth-action-btn:hover { background: #1d4ed8; }\r\n\r\n        \/* Results *\/\r\n        #pth-regex-studio-wrapper .pth-results-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; flex-wrap: wrap; gap: 8px; }\r\n        #pth-regex-studio-wrapper .pth-count { background: #dcfce7; color: #166534; padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 800; }\r\n        #pth-regex-studio-wrapper .pth-count.err { background: #fee2e2; color: #991b1b; }\r\n        #pth-regex-studio-wrapper .pth-hl-box { width: 100%; min-height: 110px; padding: 14px; font-size: 0.92rem; font-family: 'Courier New', monospace; line-height: 1.7; color: #334155; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; }\r\n        #pth-regex-studio-wrapper mark.pth-hl { background-color: #bfdbfe; color: #1e3a8a; border-radius: 3px; padding: 0 2px; }\r\n        #pth-regex-studio-wrapper mark.pth-hl.alt { background-color: #fde68a; color: #854d0e; }\r\n\r\n        #pth-regex-studio-wrapper .pth-out-box { border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; margin-top: 14px; }\r\n        #pth-regex-studio-wrapper .pth-out-head { padding: 9px 14px; font-size: 0.78rem; font-weight: 800; letter-spacing: 0.4px; text-transform: uppercase; display: flex; justify-content: space-between; align-items: center; gap: 8px; background: #eff6ff; color: #1d4ed8; border-bottom: 1px solid #bfdbfe; }\r\n        #pth-regex-studio-wrapper .pth-copy-btn { background: rgba(255,255,255,0.75); border: 1px solid rgba(0,0,0,0.08); color: #0f172a; font-size: 0.72rem; font-weight: 700; padding: 3px 9px; border-radius: 5px; cursor: pointer; font-family: inherit; text-transform: none; letter-spacing: 0; }\r\n        #pth-regex-studio-wrapper .pth-copy-btn:hover { background: #ffffff; }\r\n        #pth-regex-studio-wrapper .pth-out-body { margin: 0; padding: 14px; font-family: 'Courier New', monospace; font-size: 0.9rem; white-space: pre-wrap; word-wrap: break-word; background: #ffffff; min-height: 44px; color: #1e293b; }\r\n\r\n        \/* Table *\/\r\n        #pth-regex-studio-wrapper .pth-details { margin-top: 20px; max-height: 300px; overflow-y: auto; border: 1px solid #e2e8f0; border-radius: 8px; }\r\n        #pth-regex-studio-wrapper table.pth-table { width: 100%; border-collapse: collapse; text-align: left; font-size: 0.84rem; }\r\n        #pth-regex-studio-wrapper .pth-table th { background: #f1f5f9; color: #334155; padding: 10px 12px; font-weight: 800; position: sticky; top: 0; border-bottom: 1px solid #cbd5e1; }\r\n        #pth-regex-studio-wrapper .pth-table td { padding: 10px 12px; border-bottom: 1px solid #e2e8f0; font-family: monospace; color: #0f172a; vertical-align: top; }\r\n        #pth-regex-studio-wrapper .pth-grp-badge { display: inline-block; background: #e0e7ff; color: #3730a3; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; margin: 0 4px 4px 0; }\r\n        #pth-regex-studio-wrapper .pth-muted { color: #334155; }\r\n\r\n        \/* Explain *\/\r\n        #pth-regex-studio-wrapper .pth-explain-row { display: flex; gap: 12px; padding: 9px 0; border-bottom: 1px dashed #cbd5e1; font-size: 0.88rem; }\r\n        #pth-regex-studio-wrapper .pth-explain-row:last-child { border-bottom: none; }\r\n        #pth-regex-studio-wrapper .pth-explain-tok { font-family: monospace; font-weight: 800; color: #1d4ed8; background: #eff6ff; padding: 2px 8px; border-radius: 5px; flex: 0 0 auto; height: fit-content; }\r\n        #pth-regex-studio-wrapper .pth-explain-desc { color: #1e293b; font-weight: 500; }\r\n\r\n        \/* Side *\/\r\n        #pth-regex-studio-wrapper .pth-side-title { font-size: 0.98rem; font-weight: 800; margin: 0 0 12px; color: #0f172a; }\r\n        #pth-regex-studio-wrapper .pth-side-title.mt { margin-top: 22px; }\r\n        #pth-regex-studio-wrapper .pth-lib-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }\r\n        #pth-regex-studio-wrapper .pth-lib-btn { background: #ffffff; border: 1px solid #cbd5e1; color: #0f172a; padding: 9px 12px; border-radius: 6px; font-size: 0.85rem; font-weight: 700; text-align: left; cursor: pointer; transition: all 0.2s; display: flex; flex-direction: column; gap: 3px; font-family: inherit; }\r\n        #pth-regex-studio-wrapper .pth-lib-btn:hover { background: #eff6ff; border-color: #93c5fd; }\r\n        #pth-regex-studio-wrapper .pth-lib-desc { font-size: 0.74rem; color: #334155; font-family: monospace; font-weight: 500; }\r\n        #pth-regex-studio-wrapper table.pth-cheat { width: 100%; border-collapse: collapse; }\r\n        #pth-regex-studio-wrapper .pth-cheat th, #pth-regex-studio-wrapper .pth-cheat td { text-align: left; padding: 6px 0; border-bottom: 1px solid #e2e8f0; font-size: 0.8rem; }\r\n        #pth-regex-studio-wrapper .pth-cheat th { color: #0f172a; font-weight: 800; }\r\n        #pth-regex-studio-wrapper .pth-cheat td { color: #334155; font-weight: 500; }\r\n        #pth-regex-studio-wrapper .pth-code { background: #e2e8f0; color: #0f172a; padding: 2px 5px; border-radius: 4px; font-family: monospace; font-size: 0.82rem; font-weight: 700; }\r\n        #pth-regex-studio-wrapper .pth-note { background: #fffbeb; border: 1px solid #fde68a; padding: 12px; border-radius: 8px; font-size: 0.84rem; color: #92400e; line-height: 1.55; font-weight: 500; margin-top: 18px; }\r\n        #pth-regex-studio-wrapper .pth-related a { color: #1d4ed8; text-decoration: none; font-weight: 700; }\r\n        #pth-regex-studio-wrapper .pth-related a:hover { text-decoration: underline; }\r\n        #pth-regex-studio-wrapper .pth-related li { margin-bottom: 7px; font-size: 0.85rem; }\r\n\r\n        \/* Toast *\/\r\n        #pth-regex-studio-wrapper .pth-toast-wrap { position: fixed; bottom: 20px; right: 20px; z-index: 999999; display: flex; flex-direction: column; gap: 8px; }\r\n        #pth-regex-studio-wrapper .pth-toast { padding: 12px 18px; border-radius: 8px; color: #ffffff; font-size: 0.88rem; font-weight: 700; box-shadow: 0 8px 24px rgba(0,0,0,0.18); }\r\n        #pth-regex-studio-wrapper .pth-toast.success { background: #059669; }\r\n        #pth-regex-studio-wrapper .pth-toast.error { background: #dc2626; }\r\n        #pth-regex-studio-wrapper .pth-toast.info { background: #2563eb; }\r\n\r\n        @media (max-width: 900px) {\r\n            #pth-regex-studio-wrapper .pth-grid { grid-template-columns: 1fr; }\r\n            #pth-regex-studio-wrapper .pth-main { border-right: none; border-bottom: 1px solid #e2e8f0; }\r\n        }\r\n    <\/style>\r\n\r\n    <div class=\"pth-topbar\">\r\n        <h2 class=\"pth-title\">All-in-One Regex Studio Pro<\/h2>\r\n        <div class=\"pth-toolbar\">\r\n            <button type=\"button\" class=\"pth-btn pth-sample-btn\" id=\"pth-rx-sample\">\ud83d\udca1 Load Sample<\/button>\r\n            <button type=\"button\" class=\"pth-btn pth-fw-btn\" id=\"pth-rx-fw\" aria-label=\"Toggle full width view\">\u26f6 Full Width<\/button>\r\n            <button type=\"button\" class=\"pth-btn pth-clear-btn\" id=\"pth-rx-clear\">\ud83d\uddd1\ufe0f Clear<\/button>\r\n        <\/div>\r\n    <\/div>\r\n    <div class=\"pth-meta-bar\">\r\n        <span class=\"pth-version-badge\">REGEX STUDIO v3.0<\/span>\r\n        <span class=\"pth-status-offline\">\ud83d\udd12 100% Offline<\/span>\r\n    <\/div>\r\n\r\n    <div class=\"pth-tabbar\" role=\"tablist\">\r\n        <button type=\"button\" class=\"pth-tab-btn active\" data-tab=\"test\" role=\"tab\">\ud83c\udfaf Tester<\/button>\r\n        <button type=\"button\" class=\"pth-tab-btn\" data-tab=\"replace\" role=\"tab\">\ud83d\udd01 Replace<\/button>\r\n        <button type=\"button\" class=\"pth-tab-btn\" data-tab=\"extract\" role=\"tab\">\ud83d\udccb Extract<\/button>\r\n        <button type=\"button\" class=\"pth-tab-btn\" data-tab=\"build\" role=\"tab\">\ud83e\ude84 Builder<\/button>\r\n        <button type=\"button\" class=\"pth-tab-btn\" data-tab=\"explain\" role=\"tab\">\ud83d\udcd6 Explain<\/button>\r\n    <\/div>\r\n\r\n    <div class=\"pth-grid\" id=\"pth-rx-grid\">\r\n        <div class=\"pth-main\">\r\n\r\n            <!-- TAB 1: TESTER -->\r\n            <div class=\"pth-panel active\" id=\"pth-panel-test\" role=\"tabpanel\">\r\n                <p class=\"pth-panel-intro\">Type a pattern and paste some text. Matches highlight live, and every capture group is listed below &mdash; all in your browser.<\/p>\r\n                <div class=\"pth-field\">\r\n                    <label for=\"pth-t-pattern\">Regular Expression<\/label>\r\n                    <div class=\"pth-rx-wrap\">\r\n                        <span class=\"pth-rx-slash\">\/<\/span>\r\n                        <input type=\"text\" id=\"pth-t-pattern\" class=\"pth-rx-pattern\" aria-label=\"Regex pattern\" placeholder=\"Enter regex pattern...\">\r\n                        <span class=\"pth-rx-slash\">\/<\/span>\r\n                        <input type=\"text\" id=\"pth-t-flags\" class=\"pth-rx-flags\" aria-label=\"Regex flags\" placeholder=\"gi\">\r\n                    <\/div>\r\n                <\/div>\r\n                <div class=\"pth-field\">\r\n                    <label for=\"pth-t-text\">Test String<\/label>\r\n                    <p class=\"pth-hint\">\u2139\ufe0f Placeholder text is just an example &mdash; paste your own, or click \ud83d\udca1 Load Sample above.<\/p>\r\n                    <textarea id=\"pth-t-text\" class=\"pth-ta\" aria-label=\"Test string\" placeholder=\"Paste text to test against the pattern...\"><\/textarea>\r\n                <\/div>\r\n                <div class=\"pth-results-head\">\r\n                    <label style=\"font-weight:700; color:#334155; margin:0;\">Highlighted Results<\/label>\r\n                    <span id=\"pth-t-count\" class=\"pth-count\">0 matches<\/span>\r\n                <\/div>\r\n                <div id=\"pth-t-hl\" class=\"pth-hl-box\" aria-live=\"polite\"><\/div>\r\n                <div class=\"pth-details\" id=\"pth-t-details\" style=\"display:none;\">\r\n                    <table class=\"pth-table\">\r\n                        <thead><tr><th style=\"width:14%;\">#<\/th><th style=\"width:38%;\">Full Match<\/th><th style=\"width:48%;\">Capture Groups<\/th><\/tr><\/thead>\r\n                        <tbody id=\"pth-t-tbody\"><\/tbody>\r\n                    <\/table>\r\n                <\/div>\r\n            <\/div>\r\n\r\n            <!-- TAB 2: REPLACE -->\r\n            <div class=\"pth-panel\" id=\"pth-panel-replace\" role=\"tabpanel\">\r\n                <p class=\"pth-panel-intro\">Find with a pattern and rewrite with a replacement. Backreferences like <span class=\"pth-code\">$1<\/span> and <span class=\"pth-code\">$&amp;<\/span> work just as they do in JavaScript.<\/p>\r\n                <div class=\"pth-field\">\r\n                    <label for=\"pth-r-pattern\">Regular Expression<\/label>\r\n                    <div class=\"pth-rx-wrap\">\r\n                        <span class=\"pth-rx-slash\">\/<\/span>\r\n                        <input type=\"text\" id=\"pth-r-pattern\" class=\"pth-rx-pattern\" aria-label=\"Replace pattern\" placeholder=\"Pattern to find...\">\r\n                        <span class=\"pth-rx-slash\">\/<\/span>\r\n                        <input type=\"text\" id=\"pth-r-flags\" class=\"pth-rx-flags\" aria-label=\"Replace flags\" placeholder=\"g\">\r\n                    <\/div>\r\n                <\/div>\r\n                <div class=\"pth-field\">\r\n                    <label for=\"pth-r-replacement\">Replacement<\/label>\r\n                    <input type=\"text\" id=\"pth-r-replacement\" class=\"pth-inp mono\" aria-label=\"Replacement string\" placeholder=\"e.g. [$1] or ***\">\r\n                <\/div>\r\n                <div class=\"pth-field\">\r\n                    <label for=\"pth-r-text\">Source Text<\/label>\r\n                    <textarea id=\"pth-r-text\" class=\"pth-ta\" aria-label=\"Source text\" placeholder=\"Paste the text to transform...\"><\/textarea>\r\n                <\/div>\r\n                <div class=\"pth-out-box\">\r\n                    <div class=\"pth-out-head\"><span id=\"pth-r-count\">Result<\/span><button type=\"button\" class=\"pth-copy-btn\" data-copy=\"pth-r-out\">Copy<\/button><\/div>\r\n                    <pre class=\"pth-out-body\" id=\"pth-r-out\">Awaiting input...<\/pre>\r\n                <\/div>\r\n            <\/div>\r\n\r\n            <!-- TAB 3: EXTRACT -->\r\n            <div class=\"pth-panel\" id=\"pth-panel-extract\" role=\"tabpanel\">\r\n                <p class=\"pth-panel-intro\">Pull every match (or one capture group) into a clean list &mdash; ideal for scraping emails, URLs or IDs out of a wall of text.<\/p>\r\n                <div class=\"pth-field\">\r\n                    <label for=\"pth-e-pattern\">Regular Expression<\/label>\r\n                    <div class=\"pth-rx-wrap\">\r\n                        <span class=\"pth-rx-slash\">\/<\/span>\r\n                        <input type=\"text\" id=\"pth-e-pattern\" class=\"pth-rx-pattern\" aria-label=\"Extract pattern\" placeholder=\"Pattern to extract...\">\r\n                        <span class=\"pth-rx-slash\">\/<\/span>\r\n                        <input type=\"text\" id=\"pth-e-flags\" class=\"pth-rx-flags\" aria-label=\"Extract flags\" placeholder=\"gi\">\r\n                    <\/div>\r\n                <\/div>\r\n                <div class=\"pth-row\" style=\"margin-bottom:14px;\">\r\n                    <div class=\"pth-field\">\r\n                        <label for=\"pth-e-group\">Extract<\/label>\r\n                        <select id=\"pth-e-group\" class=\"pth-sel\" aria-label=\"Which part to extract\">\r\n                            <option value=\"0\">Full match<\/option>\r\n                            <option value=\"1\">Capture group 1<\/option>\r\n                            <option value=\"2\">Capture group 2<\/option>\r\n                            <option value=\"3\">Capture group 3<\/option>\r\n                        <\/select>\r\n                    <\/div>\r\n                <\/div>\r\n                <div class=\"pth-field\">\r\n                    <label style=\"margin-bottom:4px;\">Options<\/label>\r\n                    <label class=\"pth-check\"><input type=\"checkbox\" id=\"pth-e-dedupe\"> Remove duplicates<\/label>\r\n                    <label class=\"pth-check\"><input type=\"checkbox\" id=\"pth-e-sort\"> Sort A&rarr;Z<\/label>\r\n                <\/div>\r\n                <div class=\"pth-field\">\r\n                    <label for=\"pth-e-text\">Source Text<\/label>\r\n                    <textarea id=\"pth-e-text\" class=\"pth-ta\" aria-label=\"Source text to extract from\" placeholder=\"Paste the text to extract from...\"><\/textarea>\r\n                <\/div>\r\n                <button type=\"button\" class=\"pth-action-btn\" id=\"pth-e-btn\">\ud83d\udccb Extract Matches<\/button>\r\n                <div class=\"pth-out-box\">\r\n                    <div class=\"pth-out-head\"><span id=\"pth-e-count\">Extracted<\/span><button type=\"button\" class=\"pth-copy-btn\" data-copy=\"pth-e-out\">Copy<\/button><\/div>\r\n                    <pre class=\"pth-out-body\" id=\"pth-e-out\">Awaiting extraction...<\/pre>\r\n                <\/div>\r\n            <\/div>\r\n\r\n            <!-- TAB 4: BUILDER -->\r\n            <div class=\"pth-panel\" id=\"pth-panel-build\" role=\"tabpanel\">\r\n                <p class=\"pth-panel-intro\">Answer a few questions and the studio writes the pattern for you, then drops it into the Tester tab ready to run.<\/p>\r\n                <div class=\"pth-row\" style=\"margin-bottom:14px;\">\r\n                    <div class=\"pth-field\">\r\n                        <label for=\"pth-b-type\">Must contain<\/label>\r\n                        <select id=\"pth-b-type\" class=\"pth-sel\" aria-label=\"Character type\">\r\n                            <option value=\"any\">Any character ( . )<\/option>\r\n                            <option value=\"alpha\">Letters only (a-z, A-Z)<\/option>\r\n                            <option value=\"numeric\">Digits only (0-9)<\/option>\r\n                            <option value=\"alphanumeric\">Letters &amp; digits<\/option>\r\n                            <option value=\"word\">Word characters ( \\w )<\/option>\r\n                        <\/select>\r\n                    <\/div>\r\n                    <div class=\"pth-field\">\r\n                        <label for=\"pth-b-min\">Min length<\/label>\r\n                        <input type=\"number\" id=\"pth-b-min\" class=\"pth-inp\" min=\"0\" aria-label=\"Minimum length\" placeholder=\"1\">\r\n                    <\/div>\r\n                    <div class=\"pth-field\">\r\n                        <label for=\"pth-b-max\">Max length<\/label>\r\n                        <input type=\"number\" id=\"pth-b-max\" class=\"pth-inp\" min=\"0\" aria-label=\"Maximum length\" placeholder=\"(open)\">\r\n                    <\/div>\r\n                <\/div>\r\n                <div class=\"pth-row\" style=\"margin-bottom:14px;\">\r\n                    <div class=\"pth-field\">\r\n                        <label for=\"pth-b-start\">Starts with (literal)<\/label>\r\n                        <input type=\"text\" id=\"pth-b-start\" class=\"pth-inp mono\" aria-label=\"Starts with\" placeholder=\"e.g. https\">\r\n                    <\/div>\r\n                    <div class=\"pth-field\">\r\n                        <label for=\"pth-b-end\">Ends with (literal)<\/label>\r\n                        <input type=\"text\" id=\"pth-b-end\" class=\"pth-inp mono\" aria-label=\"Ends with\" placeholder=\"e.g. .com\">\r\n                    <\/div>\r\n                <\/div>\r\n                <div class=\"pth-field\">\r\n                    <label style=\"margin-bottom:4px;\">Extras<\/label>\r\n                    <label class=\"pth-check\"><input type=\"checkbox\" id=\"pth-b-boundary\"> Whole word ( \\b )<\/label>\r\n                    <label class=\"pth-check\"><input type=\"checkbox\" id=\"pth-b-ci\"> Ignore case ( i )<\/label>\r\n                <\/div>\r\n                <div class=\"pth-out-box\" style=\"margin-bottom:16px;\">\r\n                    <div class=\"pth-out-head\"><span>Generated Pattern<\/span><\/div>\r\n                    <pre class=\"pth-out-body\" id=\"pth-b-preview\">\/.+\/gm<\/pre>\r\n                <\/div>\r\n                <button type=\"button\" class=\"pth-action-btn\" id=\"pth-b-btn\">\ud83e\ude84 Generate &amp; Send to Tester<\/button>\r\n            <\/div>\r\n\r\n            <!-- TAB 5: EXPLAIN -->\r\n            <div class=\"pth-panel\" id=\"pth-panel-explain\" role=\"tabpanel\">\r\n                <p class=\"pth-panel-intro\">Paste a pattern and read it back in plain English, token by token. Use the library on the right for battle-tested patterns.<\/p>\r\n                <div class=\"pth-field\">\r\n                    <label for=\"pth-x-pattern\">Pattern to explain<\/label>\r\n                    <div class=\"pth-rx-wrap\">\r\n                        <span class=\"pth-rx-slash\">\/<\/span>\r\n                        <input type=\"text\" id=\"pth-x-pattern\" class=\"pth-rx-pattern\" aria-label=\"Pattern to explain\" placeholder=\"e.g. ^\\d{3}-\\d{4}$\">\r\n                        <span class=\"pth-rx-slash\">\/<\/span>\r\n                        <input type=\"text\" id=\"pth-x-flags\" class=\"pth-rx-flags\" aria-label=\"Flags to explain\" placeholder=\"\">\r\n                    <\/div>\r\n                <\/div>\r\n                <div class=\"pth-out-box\">\r\n                    <div class=\"pth-out-head\"><span>Breakdown<\/span><\/div>\r\n                    <div class=\"pth-out-body\" id=\"pth-x-out\" style=\"font-family:'Inter',sans-serif;\">Type a pattern above to see each token explained.<\/div>\r\n                <\/div>\r\n            <\/div>\r\n\r\n        <\/div>\r\n\r\n        <div class=\"pth-side\">\r\n            <h3 class=\"pth-side-title\">\ud83d\udcda Pattern Library<\/h3>\r\n            <div class=\"pth-lib-grid\" id=\"pth-lib-grid\"><\/div>\r\n\r\n            <h3 class=\"pth-side-title mt\">Cheat Sheet<\/h3>\r\n            <table class=\"pth-cheat\"><tbody id=\"pth-cheat-body\"><\/tbody><\/table>\r\n\r\n            <div class=\"pth-note\"><strong>\ud83d\udd12 Fully offline.<\/strong> Every tab uses your browser's native regular-expression engine. No text you paste is ever uploaded.<\/div>\r\n\r\n            <h3 class=\"pth-side-title mt\">Related Tools<\/h3>\r\n            <ul class=\"pth-related\" style=\"list-style:none; padding:0; margin:0;\">\r\n                <li><a href=\"https:\/\/schoolict.net\/tools\/text-diff-checker-comparator\/\">Text Diff Checker<\/a><\/li>\r\n                <li><a href=\"https:\/\/schoolict.net\/tools\/free-word-counter-and-text-case-converter\/\">Word Counter &amp; Case Converter<\/a><\/li>\r\n                <li><a href=\"https:\/\/schoolict.net\/tools\/universal-url-encoder-decoder\/\">URL Encoder \/ Decoder<\/a><\/li>\r\n            <\/ul>\r\n        <\/div>\r\n    <\/div>\r\n\r\n    <div class=\"pth-toast-wrap\" id=\"pth-rx-toast-wrap\" aria-live=\"assertive\"><\/div>\r\n\r\n    <script data-no-optimize=\"1\" data-no-minify=\"1\" data-cfasync=\"false\">\r\n    (function () {\r\n        'use strict';\r\n        if (window.pthRegexStudioApp) { return; }\r\n\r\n        window.pthRegexStudioApp = {\r\n            initialized: false,\r\n\r\n            library: [\r\n                { name: 'Email address', p: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}', f: 'gi', d: 'name@site.com' },\r\n                { name: 'URL (http\/https)', p: 'https?:\/\/[^\\\\s\/$.?#].[^\\\\s]*', f: 'gi', d: 'links in text' },\r\n                { name: 'IPv4 address', p: '\\\\b(?:\\\\d{1,3}\\\\.){3}\\\\d{1,3}\\\\b', f: 'g', d: '192.168.0.1' },\r\n                { name: 'Date (YYYY-MM-DD)', p: '\\\\d{4}-\\\\d{2}-\\\\d{2}', f: 'g', d: '2026-07-06' },\r\n                { name: 'Hex colour', p: '#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})\\\\b', f: 'g', d: '#2563eb' },\r\n                { name: 'URL slug', p: '^[a-z0-9]+(?:-[a-z0-9]+)*$', f: '', d: 'my-post-title' },\r\n                { name: 'Username 3-16', p: '^[a-zA-Z0-9_]{3,16}$', f: '', d: 'word chars only' },\r\n                { name: 'Phone (loose)', p: '\\\\+?\\\\d[\\\\d .()-]{7,}\\\\d', f: 'g', d: 'intl numbers' },\r\n                { name: 'Duplicate word', p: '\\\\b(\\\\w+)\\\\s+\\\\1\\\\b', f: 'gi', d: 'the the' },\r\n                { name: 'HTML tag', p: '<\\\\\/?[a-z][\\\\s\\\\S]*?>', f: 'gi', d: 'find elements' }\r\n            ],\r\n\r\n            cheat: [\r\n                { c: '.', d: 'Any character' },\r\n                { c: '\\\\w \\\\d \\\\s', d: 'Word \/ digit \/ space' },\r\n                { c: '\\\\W \\\\D \\\\S', d: 'Negated versions' },\r\n                { c: '[abc]', d: 'Any of a, b, c' },\r\n                { c: '[^abc]', d: 'None of a, b, c' },\r\n                { c: '^ $', d: 'Start \/ end' },\r\n                { c: '* + ?', d: 'Zero+ \/ one+ \/ optional' },\r\n                { c: '{2,5}', d: '2 to 5 times' },\r\n                { c: '(...)', d: 'Capture group' },\r\n                { c: '(?:...)', d: 'Non-capturing' },\r\n                { c: 'a|b', d: 'a OR b' },\r\n                { c: '\\\\b', d: 'Word boundary' }\r\n            ],\r\n\r\n            init: function () {\r\n                this.wrapper = document.getElementById('pth-regex-studio-wrapper');\r\n                if (!this.wrapper) { return; }\r\n                this.grid = document.getElementById('pth-rx-grid');\r\n                this.activeTab = 'test';\r\n                this.renderSidebar();\r\n                this.attachEvents();\r\n                this.buildPattern();\r\n                this.applyDeepLink();\r\n            },\r\n\r\n            applyDeepLink: function () {\r\n                try {\r\n                    var params = new URLSearchParams(window.location.search);\r\n                    var t = params.get('tab');\r\n                    if (t) { this.switchTab(t); }\r\n                } catch (e) {}\r\n            },\r\n\r\n            renderSidebar: function () {\r\n                var self = this;\r\n                var grid = document.getElementById('pth-lib-grid');\r\n                var i, btn, item;\r\n                for (i = 0; i < this.library.length; i++) {\r\n                    item = this.library[i];\r\n                    btn = document.createElement('button');\r\n                    btn.type = 'button';\r\n                    btn.className = 'pth-lib-btn';\r\n                    btn.setAttribute('data-idx', String(i));\r\n                    btn.appendChild(this.makeText('span', '', item.name));\r\n                    btn.appendChild(this.makeText('span', 'pth-lib-desc', item.d));\r\n                    grid.appendChild(btn);\r\n                }\r\n                grid.addEventListener('click', function (e) {\r\n                    var target = e.target;\r\n                    while (target && target !== grid && !target.getAttribute('data-idx')) { target = target.parentNode; }\r\n                    if (target && target.getAttribute('data-idx')) {\r\n                        self.useLibrary(parseInt(target.getAttribute('data-idx'), 10));\r\n                    }\r\n                });\r\n\r\n                var body = document.getElementById('pth-cheat-body');\r\n                for (i = 0; i < this.cheat.length; i++) {\r\n                    var tr = document.createElement('tr');\r\n                    var tdC = document.createElement('td');\r\n                    var code = this.makeText('span', 'pth-code', this.cheat[i].c);\r\n                    tdC.appendChild(code);\r\n                    var tdD = this.makeText('td', '', this.cheat[i].d);\r\n                    tr.appendChild(tdC);\r\n                    tr.appendChild(tdD);\r\n                    body.appendChild(tr);\r\n                }\r\n            },\r\n\r\n            makeText: function (tag, cls, txt) {\r\n                var el = document.createElement(tag);\r\n                if (cls) { el.className = cls; }\r\n                el.textContent = txt;\r\n                return el;\r\n            },\r\n\r\n            useLibrary: function (idx) {\r\n                var item = this.library[idx];\r\n                if (!item) { return; }\r\n                this.switchTab('test');\r\n                document.getElementById('pth-t-pattern').value = item.p;\r\n                document.getElementById('pth-t-flags').value = item.f;\r\n                this.runTest();\r\n                this.showToast('Loaded: ' + item.name, 'success');\r\n            },\r\n\r\n            attachEvents: function () {\r\n                var self = this;\r\n                var w = this.wrapper;\r\n\r\n                var tabs = w.querySelectorAll('.pth-tab-btn');\r\n                for (var i = 0; i < tabs.length; i++) {\r\n                    tabs[i].addEventListener('click', function () { self.switchTab(this.getAttribute('data-tab')); });\r\n                }\r\n\r\n                document.getElementById('pth-rx-sample').addEventListener('click', function () { self.loadSample(); });\r\n                document.getElementById('pth-rx-clear').addEventListener('click', function () { self.clearActive(); });\r\n                document.getElementById('pth-rx-fw').addEventListener('click', function () { self.toggleWidth(); });\r\n\r\n                w.addEventListener('click', function (e) {\r\n                    var t = e.target;\r\n                    if (t && t.classList && t.classList.contains('pth-copy-btn')) { self.copyFrom(t.getAttribute('data-copy')); }\r\n                });\r\n\r\n                \/\/ Tester live\r\n                this.bindInput('pth-t-pattern', function () { self.runTest(); });\r\n                this.bindInput('pth-t-flags', function () { self.runTest(); });\r\n                this.bindInput('pth-t-text', function () { self.runTest(); });\r\n\r\n                \/\/ Replace live\r\n                this.bindInput('pth-r-pattern', function () { self.runReplace(); });\r\n                this.bindInput('pth-r-flags', function () { self.runReplace(); });\r\n                this.bindInput('pth-r-replacement', function () { self.runReplace(); });\r\n                this.bindInput('pth-r-text', function () { self.runReplace(); });\r\n\r\n                \/\/ Extract\r\n                document.getElementById('pth-e-btn').addEventListener('click', function () { self.runExtract(); });\r\n\r\n                \/\/ Builder live\r\n                var bIds = ['pth-b-type', 'pth-b-min', 'pth-b-max', 'pth-b-start', 'pth-b-end', 'pth-b-boundary', 'pth-b-ci'];\r\n                for (var b = 0; b < bIds.length; b++) {\r\n                    var el = document.getElementById(bIds[b]);\r\n                    if (el) {\r\n                        el.addEventListener('input', function () { self.buildPattern(); });\r\n                        el.addEventListener('change', function () { self.buildPattern(); });\r\n                    }\r\n                }\r\n                document.getElementById('pth-b-btn').addEventListener('click', function () { self.sendBuildToTester(); });\r\n\r\n                \/\/ Explain live\r\n                this.bindInput('pth-x-pattern', function () { self.runExplain(); });\r\n                this.bindInput('pth-x-flags', function () { self.runExplain(); });\r\n            },\r\n\r\n            bindInput: function (id, fn) {\r\n                var el = document.getElementById(id);\r\n                if (el) { el.addEventListener('input', fn); }\r\n            },\r\n\r\n            switchTab: function (name) {\r\n                var valid = { test: 1, replace: 1, extract: 1, build: 1, explain: 1 };\r\n                if (!valid[name]) { return; }\r\n                this.activeTab = name;\r\n                var w = this.wrapper;\r\n                var tabs = w.querySelectorAll('.pth-tab-btn');\r\n                for (var i = 0; i < tabs.length; i++) {\r\n                    if (tabs[i].getAttribute('data-tab') === name) { tabs[i].classList.add('active'); }\r\n                    else { tabs[i].classList.remove('active'); }\r\n                }\r\n                var panels = w.querySelectorAll('.pth-panel');\r\n                for (var j = 0; j < panels.length; j++) {\r\n                    if (panels[j].id === 'pth-panel-' + name) { panels[j].classList.add('active'); }\r\n                    else { panels[j].classList.remove('active'); }\r\n                }\r\n            },\r\n\r\n            toggleWidth: function () {\r\n                this.grid.classList.toggle('full-width');\r\n                var btn = document.getElementById('pth-rx-fw');\r\n                btn.textContent = this.grid.classList.contains('full-width') ? '\\u25E7 Split View' : '\\u26F6 Full Width';\r\n            },\r\n\r\n            clearActive: function () {\r\n                var t = this.activeTab;\r\n                if (t === 'test') {\r\n                    this.setVals(['pth-t-pattern', 'pth-t-flags', 'pth-t-text']);\r\n                    this.runTest();\r\n                } else if (t === 'replace') {\r\n                    this.setVals(['pth-r-pattern', 'pth-r-flags', 'pth-r-replacement', 'pth-r-text']);\r\n                    this.runReplace();\r\n                } else if (t === 'extract') {\r\n                    this.setVals(['pth-e-pattern', 'pth-e-flags', 'pth-e-text']);\r\n                    document.getElementById('pth-e-out').textContent = 'Awaiting extraction...';\r\n                    document.getElementById('pth-e-count').textContent = 'Extracted';\r\n                } else if (t === 'build') {\r\n                    this.setVals(['pth-b-min', 'pth-b-max', 'pth-b-start', 'pth-b-end']);\r\n                    document.getElementById('pth-b-boundary').checked = false;\r\n                    document.getElementById('pth-b-ci').checked = false;\r\n                    this.buildPattern();\r\n                } else if (t === 'explain') {\r\n                    this.setVals(['pth-x-pattern', 'pth-x-flags']);\r\n                    this.runExplain();\r\n                }\r\n                this.showToast('Cleared', 'info');\r\n            },\r\n\r\n            setVals: function (ids) {\r\n                for (var i = 0; i < ids.length; i++) {\r\n                    var el = document.getElementById(ids[i]);\r\n                    if (el) { el.value = ''; }\r\n                }\r\n            },\r\n\r\n            loadSample: function () {\r\n                var t = this.activeTab;\r\n                if (t === 'test') {\r\n                    document.getElementById('pth-t-pattern').value = '([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+)';\r\n                    document.getElementById('pth-t-flags').value = 'gi';\r\n                    document.getElementById('pth-t-text').value = 'Reach us at support@primetoolhub.com or sales@example.co.uk. Old admin@test.org still forwards.';\r\n                    this.runTest();\r\n                } else if (t === 'replace') {\r\n                    document.getElementById('pth-r-pattern').value = '(\\\\d{4})-(\\\\d{2})-(\\\\d{2})';\r\n                    document.getElementById('pth-r-flags').value = 'g';\r\n                    document.getElementById('pth-r-replacement').value = '$3\/$2\/$1';\r\n                    document.getElementById('pth-r-text').value = 'Invoices: 2026-07-06, 2025-12-24 and 2024-01-01.';\r\n                    this.runReplace();\r\n                } else if (t === 'extract') {\r\n                    document.getElementById('pth-e-pattern').value = 'https?:\/\/[^\\\\s]+';\r\n                    document.getElementById('pth-e-flags').value = 'gi';\r\n                    document.getElementById('pth-e-text').value = 'Docs at https:\/\/schoolict.net\/tools and https:\/\/example.com\/page plus https:\/\/schoolict.net\/tools again.';\r\n                    document.getElementById('pth-e-dedupe').checked = true;\r\n                    this.runExtract();\r\n                } else if (t === 'build') {\r\n                    document.getElementById('pth-b-type').value = 'numeric';\r\n                    document.getElementById('pth-b-min').value = '3';\r\n                    document.getElementById('pth-b-max').value = '5';\r\n                    document.getElementById('pth-b-start').value = '';\r\n                    document.getElementById('pth-b-end').value = '';\r\n                    this.buildPattern();\r\n                } else if (t === 'explain') {\r\n                    document.getElementById('pth-x-pattern').value = '^(\\\\w+)@([a-z]+)\\\\.\\\\w{2,}$';\r\n                    document.getElementById('pth-x-flags').value = 'i';\r\n                    this.runExplain();\r\n                }\r\n                this.showToast('Sample loaded', 'success');\r\n            },\r\n\r\n            escapeHtml: function (s) {\r\n                return String(s).replace(\/&\/g, '&amp;').replace(\/<\/g, '&lt;').replace(\/>\/g, '&gt;').replace(\/\"\/g, '&quot;');\r\n            },\r\n            escapeLiteral: function (s) {\r\n                return s.replace(\/[.*+?^${}()|[\\]\\\\]\/g, '\\\\$&');\r\n            },\r\n\r\n            \/* ---------- Tester ---------- *\/\r\n            runTest: function () {\r\n                var pattern = document.getElementById('pth-t-pattern').value;\r\n                var flags = document.getElementById('pth-t-flags').value;\r\n                var text = document.getElementById('pth-t-text').value;\r\n                var hl = document.getElementById('pth-t-hl');\r\n                var count = document.getElementById('pth-t-count');\r\n                var details = document.getElementById('pth-t-details');\r\n\r\n                if (!pattern) {\r\n                    hl.textContent = text;\r\n                    count.textContent = '0 matches';\r\n                    count.className = 'pth-count';\r\n                    details.style.display = 'none';\r\n                    return;\r\n                }\r\n                try {\r\n                    var gFlags = flags.indexOf('g') !== -1 ? flags : flags + 'g';\r\n                    var rx = new RegExp(pattern, gFlags);\r\n                    var m, last = 0, out = '', list = [], idx = 0, guard = 0;\r\n                    while ((m = rx.exec(text)) !== null) {\r\n                        if (m.index === rx.lastIndex) { rx.lastIndex++; }\r\n                        if (flags.indexOf('g') === -1 && idx > 0) { break; }\r\n                        list.push(m);\r\n                        out += this.escapeHtml(text.substring(last, m.index));\r\n                        var cls = idx % 2 === 0 ? 'pth-hl' : 'pth-hl alt';\r\n                        out += '<mark class=\"' + cls + '\">' + this.escapeHtml(m[0]) + '<\/mark>';\r\n                        last = m.index + (m[0].length || 0);\r\n                        idx++;\r\n                        guard++;\r\n                        if (guard > 20000) { break; }\r\n                    }\r\n                    out += this.escapeHtml(text.substring(last));\r\n                    hl.innerHTML = out;\r\n                    if (list.length > 0) {\r\n                        count.textContent = list.length + (list.length === 1 ? ' match' : ' matches');\r\n                        count.className = 'pth-count';\r\n                        this.buildTable(list);\r\n                        details.style.display = 'block';\r\n                    } else {\r\n                        count.textContent = 'No matches';\r\n                        count.className = 'pth-count err';\r\n                        details.style.display = 'none';\r\n                    }\r\n                } catch (e) {\r\n                    hl.textContent = text;\r\n                    count.textContent = 'Invalid regex';\r\n                    count.className = 'pth-count err';\r\n                    details.style.display = 'none';\r\n                }\r\n            },\r\n\r\n            buildTable: function (matches) {\r\n                var tbody = document.getElementById('pth-t-tbody');\r\n                var html = '', k, i;\r\n                for (k = 0; k < matches.length; k++) {\r\n                    var mm = matches[k];\r\n                    var groups = '';\r\n                    if (mm.length > 1) {\r\n                        for (i = 1; i < mm.length; i++) {\r\n                            var gv = mm[i] === undefined ? '<span class=\"pth-muted\">undefined<\/span>' : this.escapeHtml(mm[i]);\r\n                            groups += '<span class=\"pth-grp-badge\">Group ' + i + '<\/span> ' + gv + '<br>';\r\n                        }\r\n                    } else {\r\n                        groups = '<span class=\"pth-muted\">No capture groups<\/span>';\r\n                    }\r\n                    html += '<tr><td>' + (k + 1) + '<br><span class=\"pth-muted\" style=\"font-size:0.72rem;\">idx ' + mm.index + '<\/span><\/td>' +\r\n                        '<td>' + this.escapeHtml(mm[0]) + '<\/td>' +\r\n                        '<td>' + groups + '<\/td><\/tr>';\r\n                }\r\n                tbody.innerHTML = html;\r\n            },\r\n\r\n            \/* ---------- Replace ---------- *\/\r\n            runReplace: function () {\r\n                var pattern = document.getElementById('pth-r-pattern').value;\r\n                var flags = document.getElementById('pth-r-flags').value;\r\n                var repl = document.getElementById('pth-r-replacement').value;\r\n                var text = document.getElementById('pth-r-text').value;\r\n                var out = document.getElementById('pth-r-out');\r\n                var count = document.getElementById('pth-r-count');\r\n\r\n                if (!pattern) {\r\n                    out.textContent = text || 'Awaiting input...';\r\n                    count.textContent = 'Result';\r\n                    return;\r\n                }\r\n                try {\r\n                    var rx = new RegExp(pattern, flags);\r\n                    var n = 0;\r\n                    var countRx = new RegExp(pattern, flags.indexOf('g') !== -1 ? flags : flags + 'g');\r\n                    var cm, guard = 0;\r\n                    while ((cm = countRx.exec(text)) !== null) {\r\n                        if (cm.index === countRx.lastIndex) { countRx.lastIndex++; }\r\n                        n++; guard++;\r\n                        if (flags.indexOf('g') === -1) { break; }\r\n                        if (guard > 50000) { break; }\r\n                    }\r\n                    var result = text.replace(rx, repl);\r\n                    out.textContent = result;\r\n                    count.textContent = n + (n === 1 ? ' replacement' : ' replacements');\r\n                } catch (e) {\r\n                    out.textContent = 'Invalid regex: ' + e.message;\r\n                    count.textContent = 'Error';\r\n                }\r\n            },\r\n\r\n            \/* ---------- Extract ---------- *\/\r\n            runExtract: function () {\r\n                var pattern = document.getElementById('pth-e-pattern').value;\r\n                var flags = document.getElementById('pth-e-flags').value;\r\n                var text = document.getElementById('pth-e-text').value;\r\n                var group = parseInt(document.getElementById('pth-e-group').value, 10);\r\n                var dedupe = document.getElementById('pth-e-dedupe').checked;\r\n                var sort = document.getElementById('pth-e-sort').checked;\r\n                var out = document.getElementById('pth-e-out');\r\n                var count = document.getElementById('pth-e-count');\r\n\r\n                if (!pattern) {\r\n                    out.textContent = 'Enter a pattern to extract.';\r\n                    count.textContent = 'Extracted';\r\n                    return;\r\n                }\r\n                try {\r\n                    var gFlags = flags.indexOf('g') !== -1 ? flags : flags + 'g';\r\n                    var rx = new RegExp(pattern, gFlags);\r\n                    var m, items = [], guard = 0;\r\n                    while ((m = rx.exec(text)) !== null) {\r\n                        if (m.index === rx.lastIndex) { rx.lastIndex++; }\r\n                        var val = m[group];\r\n                        if (val !== undefined && val !== '') { items.push(val); }\r\n                        guard++;\r\n                        if (guard > 50000) { break; }\r\n                    }\r\n                    if (dedupe) {\r\n                        var seen = {}, uniq = [];\r\n                        for (var i = 0; i < items.length; i++) {\r\n                            if (!Object.prototype.hasOwnProperty.call(seen, items[i])) {\r\n                                seen[items[i]] = 1; uniq.push(items[i]);\r\n                            }\r\n                        }\r\n                        items = uniq;\r\n                    }\r\n                    if (sort) { items.sort(); }\r\n                    if (items.length > 0) {\r\n                        out.textContent = items.join('\\n');\r\n                        count.textContent = items.length + (items.length === 1 ? ' item' : ' items');\r\n                    } else {\r\n                        out.textContent = 'No matches found for that pattern \/ group.';\r\n                        count.textContent = '0 items';\r\n                    }\r\n                } catch (e) {\r\n                    out.textContent = 'Invalid regex: ' + e.message;\r\n                    count.textContent = 'Error';\r\n                }\r\n                this.showToast('Extraction done', 'success');\r\n            },\r\n\r\n            \/* ---------- Builder ---------- *\/\r\n            computePattern: function () {\r\n                var type = document.getElementById('pth-b-type').value;\r\n                var min = document.getElementById('pth-b-min').value;\r\n                var max = document.getElementById('pth-b-max').value;\r\n                var start = document.getElementById('pth-b-start').value;\r\n                var end = document.getElementById('pth-b-end').value;\r\n                var boundary = document.getElementById('pth-b-boundary').checked;\r\n                var ci = document.getElementById('pth-b-ci').checked;\r\n\r\n                var core = '.';\r\n                if (type === 'alpha') { core = '[a-zA-Z]'; }\r\n                else if (type === 'numeric') { core = '\\\\d'; }\r\n                else if (type === 'alphanumeric') { core = '[a-zA-Z0-9]'; }\r\n                else if (type === 'word') { core = '\\\\w'; }\r\n\r\n                var quant = '+';\r\n                if (min !== '' || max !== '') { quant = '{' + (min !== '' ? min : '0') + ',' + (max !== '' ? max : '') + '}'; }\r\n\r\n                var pat = '';\r\n                if (boundary) { pat += '\\\\b'; }\r\n                if (start) { pat += this.escapeLiteral(start); }\r\n                pat += core + quant;\r\n                if (end) { pat += this.escapeLiteral(end); }\r\n                if (boundary) { pat += '\\\\b'; }\r\n\r\n                var flags = 'g';\r\n                if (ci) { flags += 'i'; }\r\n                if (start || end) { flags += 'm'; }\r\n                return { p: pat, f: flags };\r\n            },\r\n\r\n            buildPattern: function () {\r\n                var r = this.computePattern();\r\n                document.getElementById('pth-b-preview').textContent = '\/' + r.p + '\/' + r.f;\r\n            },\r\n\r\n            sendBuildToTester: function () {\r\n                var r = this.computePattern();\r\n                this.switchTab('test');\r\n                document.getElementById('pth-t-pattern').value = r.p;\r\n                document.getElementById('pth-t-flags').value = r.f;\r\n                this.runTest();\r\n                this.showToast('Pattern sent to Tester', 'success');\r\n            },\r\n\r\n            \/* ---------- Explain ---------- *\/\r\n            runExplain: function () {\r\n                var pattern = document.getElementById('pth-x-pattern').value;\r\n                var flags = document.getElementById('pth-x-flags').value;\r\n                var out = document.getElementById('pth-x-out');\r\n\r\n                if (!pattern) {\r\n                    out.textContent = 'Type a pattern above to see each token explained.';\r\n                    return;\r\n                }\r\n                var tokens = this.tokenize(pattern);\r\n                var html = '';\r\n                for (var i = 0; i < tokens.length; i++) {\r\n                    html += '<div class=\"pth-explain-row\"><span class=\"pth-explain-tok\">' + this.escapeHtml(tokens[i].t) +\r\n                        '<\/span><span class=\"pth-explain-desc\">' + this.escapeHtml(tokens[i].d) + '<\/span><\/div>';\r\n                }\r\n                if (flags) {\r\n                    html += '<div class=\"pth-explain-row\"><span class=\"pth-explain-tok\">\/' + this.escapeHtml(flags) +\r\n                        '<\/span><span class=\"pth-explain-desc\">' + this.escapeHtml(this.explainFlags(flags)) + '<\/span><\/div>';\r\n                }\r\n                out.innerHTML = html;\r\n            },\r\n\r\n            explainFlags: function (flags) {\r\n                var map = { g: 'global (all matches)', i: 'case-insensitive', m: 'multiline (^$ per line)', s: 'dotall (. matches newlines)', u: 'unicode', y: 'sticky' };\r\n                var parts = [];\r\n                for (var i = 0; i < flags.length; i++) { parts.push(flags[i] + ' = ' + (map[flags[i]] || 'unknown flag')); }\r\n                return parts.join(', ');\r\n            },\r\n\r\n            tokenize: function (p) {\r\n                var esc = {\r\n                    'd': 'a digit (0-9)', 'D': 'a non-digit', 'w': 'a word character (letter, digit or _)',\r\n                    'W': 'a non-word character', 's': 'any whitespace', 'S': 'a non-whitespace character',\r\n                    'b': 'a word boundary', 'B': 'a non-boundary', 'n': 'a newline', 't': 'a tab', 'r': 'a carriage return'\r\n                };\r\n                var out = [], i = 0, ch, next;\r\n                while (i < p.length) {\r\n                    ch = p[i];\r\n                    if (ch === '\\\\') {\r\n                        next = p[i + 1];\r\n                        if (next === undefined) { out.push({ t: '\\\\', d: 'a literal backslash' }); i++; continue; }\r\n                        if (\/[0-9]\/.test(next)) { out.push({ t: '\\\\' + next, d: 'backreference to group ' + next }); i += 2; continue; }\r\n                        if (Object.prototype.hasOwnProperty.call(esc, next)) { out.push({ t: '\\\\' + next, d: 'matches ' + esc[next] }); i += 2; continue; }\r\n                        out.push({ t: '\\\\' + next, d: 'a literal \"' + next + '\"' }); i += 2; continue;\r\n                    }\r\n                    if (ch === '[') {\r\n                        var end = p.indexOf(']', i + 1);\r\n                        if (end === -1) { end = p.length - 1; }\r\n                        var cls = p.substring(i, end + 1);\r\n                        var neg = p[i + 1] === '^';\r\n                        out.push({ t: cls, d: neg ? 'any character NOT in this set' : 'any one character in this set' });\r\n                        i = end + 1; continue;\r\n                    }\r\n                    if (ch === '(') {\r\n                        if (p.substr(i, 3) === '(?:') { out.push({ t: '(?:', d: 'start of a non-capturing group' }); i += 3; continue; }\r\n                        if (p.substr(i, 3) === '(?=') { out.push({ t: '(?=', d: 'start of a positive lookahead' }); i += 3; continue; }\r\n                        if (p.substr(i, 3) === '(?!') { out.push({ t: '(?!', d: 'start of a negative lookahead' }); i += 3; continue; }\r\n                        if (p.substr(i, 4) === '(?<=') { out.push({ t: '(?<=', d: 'start of a positive lookbehind' }); i += 4; continue; }\r\n                        if (p.substr(i, 4) === '(?<!') { out.push({ t: '(?<!', d: 'start of a negative lookbehind' }); i += 4; continue; }\r\n                        if (p.substr(i, 3) === '(?<') {\r\n                            var gclose = p.indexOf('>', i);\r\n                            if (gclose !== -1) { var nm = p.substring(i + 3, gclose); out.push({ t: p.substring(i, gclose + 1), d: 'start of named group \"' + nm + '\"' }); i = gclose + 1; continue; }\r\n                        }\r\n                        out.push({ t: '(', d: 'start of a capturing group' }); i++; continue;\r\n                    }\r\n                    if (ch === ')') { out.push({ t: ')', d: 'end of the group' }); i++; continue; }\r\n                    if (ch === '^') { out.push({ t: '^', d: 'start of the string (or line with m)' }); i++; continue; }\r\n                    if (ch === '$') { out.push({ t: '$', d: 'end of the string (or line with m)' }); i++; continue; }\r\n                    if (ch === '.') { out.push({ t: '.', d: 'any character except a newline' }); i++; continue; }\r\n                    if (ch === '|') { out.push({ t: '|', d: 'OR &mdash; match the left or right side' }); i++; continue; }\r\n                    if (ch === '*') { out.push({ t: (p[i + 1] === '?') ? '*?' : '*', d: (p[i + 1] === '?') ? 'zero or more, lazy' : 'zero or more times' }); i += (p[i + 1] === '?') ? 2 : 1; continue; }\r\n                    if (ch === '+') { out.push({ t: (p[i + 1] === '?') ? '+?' : '+', d: (p[i + 1] === '?') ? 'one or more, lazy' : 'one or more times' }); i += (p[i + 1] === '?') ? 2 : 1; continue; }\r\n                    if (ch === '?') { out.push({ t: '?', d: 'optional (zero or one)' }); i++; continue; }\r\n                    if (ch === '{') {\r\n                        var qend = p.indexOf('}', i);\r\n                        if (qend !== -1) { var q = p.substring(i, qend + 1); out.push({ t: q, d: 'repeat count ' + q }); i = qend + 1; continue; }\r\n                    }\r\n                    out.push({ t: ch, d: 'the literal character \"' + ch + '\"' });\r\n                    i++;\r\n                }\r\n                return out;\r\n            },\r\n\r\n            \/* ---------- Copy + Toast ---------- *\/\r\n            copyFrom: function (id) {\r\n                var self = this;\r\n                var el = document.getElementById(id);\r\n                if (!el) { return; }\r\n                var text = el.textContent || '';\r\n                if (!text || text.indexOf('Awaiting') === 0) { self.showToast('Nothing to copy yet', 'info'); return; }\r\n                if (navigator.clipboard && navigator.clipboard.writeText) {\r\n                    navigator.clipboard.writeText(text).then(function () { self.showToast('Copied to clipboard', 'success'); })\r\n                        .catch(function () { self.showToast('Copy blocked by browser', 'error'); });\r\n                } else { self.showToast('Clipboard not supported', 'error'); }\r\n            },\r\n\r\n            showToast: function (msg, type) {\r\n                var wrap = document.getElementById('pth-rx-toast-wrap');\r\n                if (!wrap) { return; }\r\n                var t = document.createElement('div');\r\n                t.className = 'pth-toast ' + (type || 'info');\r\n                t.setAttribute('role', 'status');\r\n                t.textContent = msg;\r\n                wrap.appendChild(t);\r\n                setTimeout(function () { if (t.parentNode) { t.parentNode.removeChild(t); } }, 3000);\r\n            }\r\n        };\r\n\r\n        function pthRxInit() {\r\n            if (!window.pthRegexStudioApp.initialized && document.getElementById('pth-regex-studio-wrapper')) {\r\n                window.pthRegexStudioApp.initialized = true;\r\n                window.pthRegexStudioApp.init();\r\n            }\r\n        }\r\n        pthRxInit();\r\n        if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', pthRxInit); }\r\n        window.addEventListener('load', function () { setTimeout(pthRxInit, 50); });\r\n        if (!window.pthRegexStudioApp.initialized && typeof MutationObserver !== 'undefined') {\r\n            var pthRxObs = new MutationObserver(function () {\r\n                if (document.getElementById('pth-regex-studio-wrapper')) { pthRxObs.disconnect(); pthRxInit(); }\r\n            });\r\n            pthRxObs.observe(document.body || document.documentElement, { childList: true, subtree: true });\r\n            setTimeout(function () { pthRxObs.disconnect(); }, 10000);\r\n        }\r\n    })();\r\n    <\/script>\r\n<\/div>\n\n\n\n\n<style>\n.pth-container{max-width:1480px;margin:0 auto;padding:0 20px;box-sizing:border-box}\n.pth-feature-wrap{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;margin-bottom:40px}\n.pth-feat-box{border:1px solid #e2e8f0;background:#fff;padding:24px;border-radius:12px;box-shadow:0 4px 6px -1px rgba(0,0,0,0.02);transition:all .3s ease}\n.pth-feat-box:hover{border-color:#bfdbfe;transform:translateY(-3px);box-shadow:0 10px 15px -3px rgba(0,0,0,0.05)}\n.pth-feat-title{font-size:1.05rem;font-weight:800;color:#0f172a;margin:0 0 10px;display:flex;align-items:center;gap:10px}\n.pth-feat-text{font-size:.9rem;color:#1e293b;line-height:1.6;margin:0;font-weight:500}\n.pth-steps-wrap{border:1px solid #e2e8f0;background:#fff;padding:40px 30px;border-radius:16px;text-align:center;box-shadow:0 4px 6px -1px rgba(0,0,0,0.02);margin-bottom:40px;width:100%;box-sizing:border-box}\n.pth-steps-title{font-size:1.2rem;font-weight:800;color:#0f172a;text-transform:uppercase;margin:0 0 35px;letter-spacing:1px}\n.pth-steps-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:30px}\n.pth-step-item{display:flex;flex-direction:column;align-items:center}\n.pth-step-num{width:45px;height:45px;background:#2563eb;color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:1.2rem;margin-bottom:15px;box-shadow:0 4px 10px rgba(37,99,235,.3)}\n.pth-step-name{font-weight:800;color:#0f172a;margin:0 0 8px;font-size:1rem}\n.pth-step-desc{font-size:.9rem;color:#1e293b;line-height:1.5;margin:0;font-weight:500}\n<\/style>\n<div class=\"pth-container\">\n  <div class=\"pth-feature-wrap\">\n    <div class=\"pth-feat-box\">\n      <p class=\"pth-feat-title\"><span aria-hidden=\"true\">\ud83d\udfe2<\/span> Test and see matches live<\/p>\n      <p class=\"pth-feat-text\">Type a pattern and matches light up in your text as you go, in alternating colours so neighbouring hits stay readable. A table underneath lists every match with its position and each capture group, which is where most real debugging actually happens.<\/p>\n    <\/div>\n    <div class=\"pth-feat-box\">\n      <p class=\"pth-feat-title\"><span aria-hidden=\"true\">\ud83d\udd35<\/span> Replace and extract in bulk<\/p>\n      <p class=\"pth-feat-text\">The Replace tab rewrites text using backreferences like $1, so reformatting dates or masking data takes one line. The Extract tab pulls every match into a clean list, with options to drop duplicates and sort, perfect for harvesting emails or links from a messy paste.<\/p>\n    <\/div>\n    <div class=\"pth-feat-box\">\n      <p class=\"pth-feat-title\"><span aria-hidden=\"true\">\ud83d\udfe3<\/span> Build and understand patterns<\/p>\n      <p class=\"pth-feat-text\">Not fluent in regex yet? The Builder writes a pattern from plain choices, and the Explain tab reads any pattern back to you token by token. A side library of ten tested patterns covers the everyday jobs so you rarely start from a blank box.<\/p>\n    <\/div>\n  <\/div>\n  <div class=\"pth-steps-wrap\">\n    <p class=\"pth-steps-title\">How to Use the Regex Studio<\/p>\n    <div class=\"pth-steps-grid\">\n      <div class=\"pth-step-item\">\n        <div class=\"pth-step-num\" aria-hidden=\"true\">1<\/div>\n        <p class=\"pth-step-name\">Tester<\/p>\n        <p class=\"pth-step-desc\">Enter a pattern and flags, paste text, and watch matches highlight with a full capture-group breakdown below.<\/p>\n      <\/div>\n      <div class=\"pth-step-item\">\n        <div class=\"pth-step-num\" aria-hidden=\"true\">2<\/div>\n        <p class=\"pth-step-name\">Replace<\/p>\n        <p class=\"pth-step-desc\">Add a replacement using $1, $2 or $&amp;. The rewritten text and a replacement count appear instantly, ready to copy.<\/p>\n      <\/div>\n      <div class=\"pth-step-item\">\n        <div class=\"pth-step-num\" aria-hidden=\"true\">3<\/div>\n        <p class=\"pth-step-name\">Extract<\/p>\n        <p class=\"pth-step-desc\">Choose the full match or a group, tick Remove duplicates or Sort, then press Extract for a clean one-per-line list.<\/p>\n      <\/div>\n      <div class=\"pth-step-item\">\n        <div class=\"pth-step-num\" aria-hidden=\"true\">4<\/div>\n        <p class=\"pth-step-name\">Builder &amp; Explain<\/p>\n        <p class=\"pth-step-desc\">Let the Builder draft a pattern, or paste one into Explain to read every token in plain English.<\/p>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n \n\n\n\n<p class=\"wp-block-paragraph\">Last updated: July 2026<\/p>\n\n\n\n<h2 id=\"\ud83d\udd34-the-pattern-looks-right-but-matches-nothing\" class=\"wp-block-heading\">\ud83d\udd34&nbsp;<strong>The pattern looks right but matches nothing<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Everyone who writes regular expressions has stared at a pattern that should obviously work while it stubbornly matches zero things. The fastest cure is to stop guessing and watch the engine work. Paste your text into the Tester tab, type the pattern, and the matches highlight the instant they land. When nothing lights up, the problem is usually one of three small things: a special character that needed a backslash, a missing flag, or a greedy quantifier swallowing more than you meant.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Take a real example. You want the price out of&nbsp;Total: $49.99&nbsp;and you try&nbsp;$\\d+. Nothing matches, because in regex the dollar sign means &#8220;end of line&#8221;, not a literal dollar. The Tester shows you the empty result at once, you escape it to&nbsp;\\$\\d+\\.\\d+, and the match appears. That loop of type, look, adjust is the whole point of a live tester, and it turns a ten-minute head-scratch into a ten-second fix.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The capture-group table below the highlight is the other half of the story. Wrap parts of your pattern in parentheses and each match row shows exactly what landed in group one, group two, and so on. That is how you confirm a pattern is grabbing the right pieces before you paste it into code, rather than after it has quietly failed in production.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe1-rewriting-text-with-backreferences-not-by-hand\" class=\"wp-block-heading\">\ud83d\udfe1&nbsp;<strong>Rewriting text with backreferences, not by hand<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Plenty of tools let you test a pattern. Far fewer let you act on it, which is why the Replace tab is the one people end up using most. It runs the same find you tested, then rewrites every hit using a replacement string, and the result updates as you type with a running count of how many replacements happened.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Backreferences are what make this powerful. Suppose a file is full of dates written&nbsp;2026-07-06&nbsp;and your report needs&nbsp;06\/07\/2026. A pattern of&nbsp;(\\d{4})-(\\d{2})-(\\d{2})&nbsp;with a replacement of&nbsp;$3\/$2\/$1&nbsp;flips all of them in one pass, however many there are. Other everyday jobs fall out the same way: mask card numbers by replacing digits with asterisks, wrap terms in brackets, or collapse repeated spaces. Because it runs on your machine, you can safely do this to text you would never paste into an unknown website.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe2-harvesting-a-clean-list-out-of-a-messy-paste\" class=\"wp-block-heading\">\ud83d\udfe2&nbsp;<strong>Harvesting a clean list out of a messy paste<\/strong><\/h2>\n\n\n<figure class=\"pth-article-figure pth-img-left\" style=\"float:left; width:700px; max-width:100%; margin:4px 28px 16px 0; clear:left;\"><img decoding=\"async\" src=\"https:\/\/schoolict.net\/tools\/wp-content\/uploads\/2026\/07\/regex-replace-extract-800x447.jpeg\" alt=\"Regex extracting a clean deduplicated list of URLs from a block of text\" width=\"700\" height=\"394\" loading=\"lazy\" data-no-lazy=\"1\" class=\"pth-article-img\" style=\"width:100%;height:auto;display:block;border-radius:10px;border:1px solid #e2e8f0;\"><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The Extract tab answers a question that comes up constantly: &#8220;there are fifty email addresses buried in this blob, can I just get them as a list?&#8221; You give it a pattern, choose whether to pull the full match or a specific capture group, and it returns one result per line. Tick Remove duplicates and the forty mentions of the same address collapse to one. Tick Sort and they come back alphabetised.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A quick input-and-output example. Paste a paragraph containing&nbsp;https:\/\/schoolict.net\/tools&nbsp;three times and one other link, run&nbsp;https?:\/\/[^\\s]+&nbsp;with duplicates removed, and you get exactly two clean URLs. From there it is a copy button away from a spreadsheet or a script. If your next step is comparing two of these lists, the&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/text-diff-checker-comparator\/\" rel=\"noreferrer noopener\" target=\"_blank\">Text Diff Checker<\/a>&nbsp;picks up neatly where extraction leaves off.<\/p>\n\n\n\n<h3 id=\"\ud83d\udfe2-three-habits-that-prevent-most-regex-pain\" class=\"wp-block-heading\">\ud83d\udfe2&nbsp;<strong>Three habits that prevent most regex pain<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\ud83d\udd35 Escape the literals. A dot, a plus, a dollar and brackets all have special meaning, so a real full stop is&nbsp;\\.&nbsp;not&nbsp;.<br>\ud83d\udfe0 Mind the greedy star.&nbsp;.*&nbsp;grabs as much as it can; add a&nbsp;?&nbsp;to make it lazy when it overshoots.<br>\ud83d\udfe3 Add the flags you need. Without&nbsp;g&nbsp;you get one match, and without&nbsp;i&nbsp;case matters.<\/p>\n\n\n\n<h2 id=\"\ud83d\udd34-when-regex-is-the-wrong-tool-and-what-to-reach-for\" class=\"wp-block-heading\">\ud83d\udd34&nbsp;<strong>When regex is the wrong tool, and what to reach for<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Regex is a scalpel for text patterns, but not every job is a pattern. The classic trap is trying to parse nested structures like full HTML or JSON with a regular expression. They are not &#8220;regular&#8221; in the formal sense, so a pattern that seems to work will break on the first unusual nesting. The library here includes a loose HTML-tag pattern for quick finds, and that is the right scope for it: locating tags, not truly parsing a document.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The studio also works on the text you give it, with no memory between runs, so it is not a search-and-replace across many files at once. For plain counting or changing case you do not need a pattern at all; the&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/free-word-counter-and-text-case-converter\/\" rel=\"noreferrer noopener\" target=\"_blank\">Word Counter and Case Converter<\/a>&nbsp;is quicker, and for encoding query strings the&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/universal-url-encoder-decoder\/\" rel=\"noreferrer noopener\" target=\"_blank\">URL Encoder \/ Decoder<\/a>&nbsp;is purpose-built. If you want to go deeper on the syntax itself, the&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Guide\/Regular_expressions\" rel=\"noreferrer noopener\" target=\"_blank\">MDN guide to regular expressions<\/a>&nbsp;is the reference this tool follows, since it uses the browser&#8217;s own JavaScript engine. Our roundup of&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/top-10-client-side-offline-web-developer-tools\/\" rel=\"noreferrer noopener\" target=\"_blank\">offline developer tools<\/a>&nbsp;shows where the studio fits alongside the rest.<\/p>\n\n\n\n\n<style>\n.pth-faq-section{margin:50px auto 40px;font-family:inherit;max-width:1480px;padding:0 20px;box-sizing:border-box}\n.pth-faq-header{font-size:1.8rem;font-weight:800;color:#0f172a;margin-bottom:25px;border-bottom:2px solid #e2e8f0;padding-bottom:10px;display:flex;align-items:center;gap:10px}\n.pth-faq-grid{display:grid;grid-template-columns:1fr;gap:20px}\n@media(min-width:768px){.pth-faq-grid{grid-template-columns:repeat(2,1fr)}}\n@media(min-width:1024px){.pth-faq-grid{grid-template-columns:repeat(3,1fr)}}\n.pth-faq-card{background:#f8fafc;padding:24px;border-radius:12px;border:1px solid #e2e8f0;transition:transform .2s ease;break-inside:avoid}\n.pth-faq-card:hover{transform:translateY(-3px);box-shadow:0 4px 12px rgba(0,0,0,.05)}\n.pth-faq-q{color:#0f172a;font-size:1rem;font-weight:700;margin:0 0 12px;line-height:1.4}\n.pth-faq-a{margin:0;font-size:.95rem;color:#1e293b;line-height:1.6;font-weight:500}\n<\/style>\n<div class=\"pth-faq-section\">\n  <div class=\"pth-faq-header\">\u2753 Frequently Asked Questions<\/div>\n  <div class=\"pth-faq-grid\">\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Which regex flavour does this use?<\/p>\n      <p class=\"pth-faq-a\">It uses your browser&#8217;s native JavaScript engine, so the syntax matches ECMAScript regular expressions. Patterns you build here behave the same way in JavaScript code.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Is my text uploaded anywhere?<\/p>\n      <p class=\"pth-faq-a\">No. Every tab runs entirely in your browser with no server calls, so you can safely test patterns against private logs, emails or data.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">How do backreferences work in Replace?<\/p>\n      <p class=\"pth-faq-a\">Wrap parts of your pattern in parentheses, then use $1, $2 and so on in the replacement to reuse them. $&amp; inserts the whole match. It is the same syntax as JavaScript&#8217;s replace method.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Why do I only get one match?<\/p>\n      <p class=\"pth-faq-a\">You are missing the global flag. Add g to the flags box and the engine finds every match instead of stopping at the first one.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">What does the Explain tab do?<\/p>\n      <p class=\"pth-faq-a\">It reads a pattern token by token and describes each part in plain English, including groups, quantifiers and character classes. It is a fast way to understand a pattern you inherited.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Can I extract just one capture group?<\/p>\n      <p class=\"pth-faq-a\">Yes. In the Extract tab, set the dropdown to Capture group 1, 2 or 3 and the list contains only that group from each match rather than the full match.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Should I parse HTML with regex?<\/p>\n      <p class=\"pth-faq-a\">Only for quick tag-finding. Regex cannot reliably parse deeply nested HTML or JSON, so use a real parser for that. The built-in HTML pattern is meant for locating tags, not full parsing.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">What is a greedy versus lazy quantifier?<\/p>\n      <p class=\"pth-faq-a\">Greedy quantifiers like .* grab as much as possible. Adding a question mark, as in .*?, makes them lazy so they take as little as possible, which fixes many over-matching problems.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Does the Builder cover advanced patterns?<\/p>\n      <p class=\"pth-faq-a\">The Builder handles common needs like character type, length and start or end text. For lookaheads and complex logic, start there, then refine the result in the Tester tab.<\/p>\n    <\/div>\n  <\/div>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>All-in-One Regex Studio Pro Test, debug, and learn Regular Expressions (Regex) securely offline. Features real-time text highlighting, capture group extraction, and a built-in cheat sheet. \ud83d\udfe2 Test and see matches live Type a pattern and matches light up in your text as you go, in alternating colours so neighbouring hits stay readable. A table underneath &#8230; <a title=\"Advanced Regex Tester | Test, Replace &amp; Extract Patterns Offline\" class=\"read-more\" href=\"https:\/\/schoolict.net\/tools\/advanced-text-regex-tester-debugger\/\" aria-label=\"Read more about Advanced Regex Tester | Test, Replace &amp; Extract Patterns Offline\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":2131,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-763","page","type-page","status-publish","has-post-thumbnail"],"_links":{"self":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/763","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/comments?post=763"}],"version-history":[{"count":0,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/763\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media\/2131"}],"wp:attachment":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media?parent=763"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}