diff options
-rw-r--r-- | html/playground/regex.html | 477 | ||||
-rw-r--r-- | js/regex.js | 41 |
2 files changed, 504 insertions, 14 deletions
diff --git a/html/playground/regex.html b/html/playground/regex.html new file mode 100644 index 0000000..41f50e9 --- /dev/null +++ b/html/playground/regex.html @@ -0,0 +1,477 @@ +<html lang="en"><head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>JavaScript Playground</title> + <meta name="description" content="A JavaScript jungle-gym for doing experiments and sharing scrappy fiddles."> + <style> + body { + display: flex; + flex-direction: column; + align-items: center; + background-color: #ddd; + padding: 10px; + height: 100vh; + margin: 0; + } + + textarea { + width: 100%; + height: 64%; + font-family: monospace; + background-color: #FFFEEC; + border: 2px solid #000; + scrollbar-width: none; + font-size: 1rem; + margin-bottom: 10px; + padding: 10px; + box-sizing: border-box; + resize: none; + border-bottom: 12px solid teal; + -webkit-user-modify: read-write-plaintext-only; + } + + textarea::-webkit-scrollbar { + display: none; + } + + textarea::selection { + background-color: #EFECA7; + } + + textarea:focus { + outline: none; + } + + #console { + width: 100%; + height: 22%; + background-color: #000; + color: #0fc; + font-family: monospace; + font-size: 1rem; + overflow-y: auto; + padding: 10px; + box-sizing: border-box; + white-space: pre-wrap; + } + + .button-container { + width: 100%; + display: flex; + justify-content: flex-end; + margin-bottom: 10px; + } + + button { + padding: 10px 20px; + font-size: 1rem; + margin-left: 10px; + cursor: pointer; + border: none; + transition: background-color 0.3s ease; + } + + button:hover, button:focus { + outline: none; + } + + button.run { + background-color: teal; + color: #FFFFFF; + } + </style> +</head> +<body> + + <div class="playground" id="playground"> + <div class="seesaw" id="seesaw"></div> + <div class="slide" id="slide"></div> + </div> + + <div class="button-container"> + <button onclick="clearEverything()">Clear</button> + <button onclick="downloadCodeAndEditor()">Download</button> + <button onclick="shareCode()">Share</button> + <button onclick="evaluateCode()" class="run">Run Code</button> + </div> + <textarea id="codeInput">const tokenize = (pattern) => { + const tokens = []; + let i = 0; + + while (i < pattern.length) { + const char = pattern[i]; + + if (char === '.' || char === '*' || char === '(' || char === ')' || char === '|') { + tokens.push({ + type: char, + value: char + }); + } else if (char === '\\') { // Handle escaped characters + i++; + tokens.push({ + type: 'literal', + value: pattern[i] + }); + } else if (char === '[') { // Handle character classes + let charClass = ''; + i++; + while (pattern[i] !== ']' && i < pattern.length) { + charClass += pattern[i]; + i++; + } + tokens.push({ + type: 'charClass', + value: charClass + }); + } else { + tokens.push({ + type: 'literal', + value: char + }); + } + i++; + } + + return tokens; +}; + + + + +const parse = (tokens) => { + let i = 0; + + const parseSequenceExpression = () => { + const node = { + type: 'sequence', + elements: [] + }; + + while (i < tokens.length) { + const token = tokens[i]; + + if (token.type === 'literal') { + node.elements.push({ + type: 'literal', + value: token.value + }); + } else if (token.type === '*') { + const lastElement = node.elements.pop(); + node.elements.push({ + type: 'star', + element: lastElement + }); + } else if (token.type === '.') { + node.elements.push({ + type: 'dot' + }); + } else if (token.type === 'charClass') { + node.elements.push({ + type: 'charClass', + value: token.value + }); + } else if (token.type === '|') { + i++; + const right = parseSequenceExpression(); + return { + type: 'alternation', + left: node, + right + }; + } else if (token.type === '(') { + i++; + node.elements.push(parseSequenceExpression()); + } else if (token.type === ')') { + break; // End of a grouping + } + + i++; + } + + return node; + }; + + return parseSequenceExpression(); +}; + + + +const evaluateMatch = (node) => (input) => { + if (node.type === 'literal') { + return input[0] === node.value ? input.slice(1) : null; + } + if (node.type === 'dot') { + return input.length > 0 ? input.slice(1) : null; + } + if (node.type === 'star') { + let remainder = input; + while (remainder !== null) { + const next = evaluateMatch(node.element)(remainder); + if (next === null) { + break; + } + remainder = next; + } + return remainder; + } + if (node.type === 'charClass') { + return node.value.includes(input[0]) ? input.slice(1) : null; + } + if (node.type === 'alternation') { + const remainderLeft = evaluateMatch(node.left)(input); + const remainderRight = evaluateMatch(node.right)(input); + return remainderLeft !== null ? remainderLeft : remainderRight; + } + if (node.type === 'sequence') { + let remainder = input; + for (const element of node.elements) { + remainder = evaluateMatch(element)(remainder); + if (remainder === null) { + return null; + } + } + return remainder; + } +}; + + + + +const assertEqual = (expected, actual, message) => { + if (expected !== actual) { + console.error(`FAIL: ${message}`); + } else { + console.log(`PASS: ${message}`); + } +}; + + + +const runTests = () => { + const tests = [{ + pattern: "a.b*c", + input: "abbbc", + expected: true + }, + { + pattern: "a.b*c", + input: "abc", + expected: true + }, + { + pattern: "a.b*c", + input: "ac", + expected: true + }, + { + pattern: "a.b*c", + input: "abbc", + expected: true + }, + { + pattern: "a.b*c", + input: "axbc", + expected: false + }, + + // Character class tests + { + pattern: "[abc]x", + input: "bx", + expected: true + }, + { + pattern: "[abc]x", + input: "dx", + expected: false + }, + + // Grouping and alternation tests + { + pattern: "(a|b)c", + input: "ac", + expected: true + }, + { + pattern: "(a|b)c", + input: "bc", + expected: true + }, + { + pattern: "(a|b)c", + input: "cc", + expected: false + }, + + // Escaped characters tests + { + pattern: "a\\.b", + input: "a.b", + expected: true + }, + { + pattern: "a\\.b", + input: "a-b", + expected: false + }, + { + pattern: "a\\*b", + input: "a*b", + expected: true + } + ]; + + tests.forEach(({ pattern, input, expected }, index) => { + const tokens = tokenize(pattern); + const ast = parse(tokens); + const result = evaluateMatch(ast)(input) !== null; + assertEqual(expected, result, `Test ${index + 1}`); + }); +}; + +runTests();</textarea> + <div id="console"><div>PASS: Test 1</div><div>PASS: Test 2</div><div>PASS: Test 4</div><div>PASS: Test 6</div><div>PASS: Test 7</div><div>PASS: Test 8</div><div>PASS: Test 9</div><div>PASS: Test 10</div><div>PASS: Test 11</div><div>PASS: Test 12</div><div>PASS: Test 13</div></div> + + <script> + function evaluateCode() { + const code = document.getElementById('codeInput').value; + const consoleDiv = document.getElementById('console'); + consoleDiv.innerHTML = ''; + + // Custom console.log function to output to the console div + const originalConsoleLog = console.log; + console.log = function(...args) { + args.forEach(arg => { + const output = document.createElement('div'); + output.textContent = typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg; + consoleDiv.appendChild(output); + }); + originalConsoleLog.apply(console, args); + }; + + try { + eval(code); + } catch (error) { + const errorOutput = document.createElement('div'); + errorOutput.textContent = error; + errorOutput.style.color = 'red'; + consoleDiv.appendChild(errorOutput); + } + + // Restore browser's console.log + console.log = originalConsoleLog; + } + + function downloadCodeAndEditor() { + const codeInput = document.getElementById('codeInput').value; + const htmlContent = document.documentElement.outerHTML.replace( + /<textarea id="codeInput"[^>]*>.*<\/textarea>/, + `<textarea id="codeInput">${codeInput}</textarea>` + ); + + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'code_editor.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + function shareCode() { + const code = document.getElementById('codeInput').value; + const encodedCode = btoa(encodeURIComponent(code)); + window.location.hash = encodedCode; + window.prompt("Copy the URL to share.\nBe warned! Very long URLs don't share wicked well, sometimes.", window.location.href); + } + + function clearEverything() { + if (!confirm('Are you sure you want to reset the playground?')) { + return; + } else { + window.location.hash = ''; + window.location.reload(); + } + } + + function loadCodeFromHash() { + const hash = window.location.hash.substring(1); + if (hash) { + try { + const decodedCode = decodeURIComponent(atob(hash)); + document.getElementById('codeInput').value = decodedCode; + } catch (error) { + console.error('Failed to decode the URL hash:', error); + } + } + } + + function help() { + const helpText = ` + Welcome to the JavaScript Playground! Here are some tips to get you started: + + 1. Enter your JavaScript code in the textarea. + 2. Click the "Run Code" button to execute your code. + 3. The console output will be displayed below the textarea. + 4. Click the "Clear" button to reset the playground. + 5. Click the "Download" button to save your code and editor as an HTML file. + 6. Click the "Share" button to generate a URL to share your code with others. + 7. You can also press "Cmd + Enter" to run your code. + 8. There's an empty div above the buttons with the id "playground" + 9. You can mount stuff to it using the "mount" function, for more info run "mountHelp()" + 10. You can use the "clear()" function to clear the content's of the console. + + Go nuts! Share scrappy fiddles! + `; + console.log(helpText); + } + + function clear() { + document.getElementById('console').innerHTML = ''; + } + + function mountHelp() { + console.log(` + The mount function is used to mount stuff to the playground div. + It takes a function as an argument, which in turn receives the playground div as an argument. + Before mounting, it clears the playground div. + Here's an example of how to use the mount function: + + mount(playground => { + const h1 = document.createElement('h1'); + h1.textContent = 'Hell is empty and all the devils are here.'; + playground.appendChild(h1); + }); + + This will add an h1 element to the playground div. + `); + } + + function mount(mountFunction) { + const playground = document.getElementById('playground'); + if (!playground) { + console.error("Couldn't find a div with the id 'playground'! You may need to reload the page."); + return; + } + + if (playground.innerHTML.trim() !== "") { + playground.innerHTML = ""; + } + mountFunction(playground); + } + + + document.getElementById('codeInput').addEventListener('keydown', function(event) { + if (event.metaKey && event.key === 'Enter') { + event.preventDefault(); + evaluateCode(); + } + }); + + window.onload = loadCodeFromHash; + </script> + + +</body></html> \ No newline at end of file diff --git a/js/regex.js b/js/regex.js index 14199af..86acf21 100644 --- a/js/regex.js +++ b/js/regex.js @@ -7,7 +7,8 @@ const tokenize = (pattern) => { if (char === '.' || char === '*' || char === '(' || char === ')' || char === '|') { tokens.push({ - type: char + type: char, + value: char }); } else if (char === '\\') { // Handle escaped characters i++; @@ -40,10 +41,11 @@ const tokenize = (pattern) => { + const parse = (tokens) => { let i = 0; - const parseExpression = () => { + const parseSequenceExpression = () => { const node = { type: 'sequence', elements: [] @@ -74,7 +76,7 @@ const parse = (tokens) => { }); } else if (token.type === '|') { i++; - const right = parseExpression(); + const right = parseSequenceExpression(); return { type: 'alternation', left: node, @@ -82,9 +84,9 @@ const parse = (tokens) => { }; } else if (token.type === '(') { i++; - node.elements.push(parseExpression()); + node.elements.push(parseSequenceExpression()); } else if (token.type === ')') { - break; // End of grouping + break; // End of a grouping } i++; @@ -93,12 +95,12 @@ const parse = (tokens) => { return node; }; - return parseExpression(); + return parseSequenceExpression(); }; -const match = (node, input) => { +const evaluateMatch = (node) => (input) => { if (node.type === 'literal') { return input[0] === node.value ? input.slice(1) : null; } @@ -108,7 +110,7 @@ const match = (node, input) => { if (node.type === 'star') { let remainder = input; while (remainder !== null) { - const next = match(node.element, remainder); + const next = evaluateMatch(node.element)(remainder); if (next === null) { break; } @@ -120,14 +122,14 @@ const match = (node, input) => { return node.value.includes(input[0]) ? input.slice(1) : null; } if (node.type === 'alternation') { - const remainderLeft = match(node.left, input); - const remainderRight = match(node.right, input); + const remainderLeft = evaluateMatch(node.left)(input); + const remainderRight = evaluateMatch(node.right)(input); return remainderLeft !== null ? remainderLeft : remainderRight; } if (node.type === 'sequence') { let remainder = input; for (const element of node.elements) { - remainder = match(element, remainder); + remainder = evaluateMatch(element)(remainder); if (remainder === null) { return null; } @@ -138,6 +140,17 @@ const match = (node, input) => { + +const assertEqual = (expected, actual, message) => { + if (expected !== actual) { + console.error(`FAIL: ${message}`); + } else { + console.log(`PASS: ${message}`); + } +}; + + + const runTests = () => { const tests = [{ pattern: "a.b*c", @@ -212,11 +225,11 @@ const runTests = () => { } ]; - tests.forEach(({pattern,input,expected}, index) => { + tests.forEach(({ pattern, input, expected }, index) => { const tokens = tokenize(pattern); const ast = parse(tokens); - const result = match(ast, input) !== null; - console.log(`Test ${index + 1}: ${result === expected ? 'PASS' : 'FAIL'}`); + const result = evaluateMatch(ast)(input) !== null; + assertEqual(expected, result, `Test ${index + 1}`); }); }; |