From 8a5a19d9e75dde64c65476c8e089dd4eed4dfff1 Mon Sep 17 00:00:00 2001 From: Steven Levithan Date: Fri, 8 Nov 2024 02:18:47 +0100 Subject: [PATCH] Demo: Auto-compare to all other targets/accuracies --- README.md | 28 +++---- demo/demo.css | 5 +- demo/demo.js | 126 ++++++++++++++++++++++++------- demo/index.html | 7 +- spec/match-backreference.spec.js | 2 +- 5 files changed, 119 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 271f82e..e7783f6 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,19 @@ The returned `flags` (as well as the `pattern`, of course) might be different th If the only keys returned are `pattern` and `flags`, they can optionally be provided to JavaScript's `RegExp` constructor instead. Setting option `avoidSubclass` to `true` ensures that this is always the case, and any patterns that rely on `EmulatedRegExp`'s additional handling for emulation throw an error. +### `toOnigurumaAst` + +Generates an Oniguruma AST from an Oniguruma pattern. + +```ts +function toOnigurumaAst( + pattern: string, + options?: { + flags?: OnigurumaFlags; + } +): OnigurumaAst; +``` + ### `EmulatedRegExp` Works the same as the native JavaScript `RegExp` constructor in all contexts, but can be provided results from `toDetails` to produce the same result as `toRegExp`. @@ -135,19 +148,6 @@ class EmulatedRegExp extends RegExp { }; ``` -### `toOnigurumaAst` - -Generates an Oniguruma AST from an Oniguruma pattern. - -```ts -function toOnigurumaAst( - pattern: string, - options?: { - flags?: OnigurumaFlags; - } -): OnigurumaAst; -``` - ## 🔩 Options The following options are shared by functions [`toRegExp`](#toregexp) and [`toDetails`](#todetails). @@ -620,7 +620,7 @@ Notice that nearly every feature below has at least subtle differences from Java ✅ ✅ - ✔ Same as JS ^ $ without flag m
+ ✔ Same as JS ^ $ without JS flag m
diff --git a/demo/demo.css b/demo/demo.css index bd8f4e2..fb3dbdc 100644 --- a/demo/demo.css +++ b/demo/demo.css @@ -200,7 +200,6 @@ pre, code, kbd, textarea { background: repeating-linear-gradient(45deg, #edfff1, #edfff1 3px, #deffd5 3px, #deffd5 10px); } -#info { - margin-top: -12px; - padding: 0.6em; +.info { + font-size: 0.9em; } diff --git a/demo/demo.js b/demo/demo.js index 62b93e8..865aa10 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -1,3 +1,9 @@ +const ui = { + input: document.getElementById('input'), + output: document.getElementById('output'), + subclassInfo: document.getElementById('subclass-info'), + alternateInfo: document.getElementById('alternate-info'), +}; const state = { flags: { i: getValue('flag-i'), @@ -15,44 +21,95 @@ const state = { }, }; -const inputEl = document.getElementById('input'); -autoGrow(inputEl); -showOutput(inputEl); +autoGrow(); +showTranspiled(); + +function autoGrow() { + ui.input.style.height = '0'; + ui.input.style.height = (ui.input.scrollHeight + 5) + 'px'; +} -function showOutput(el) { - const input = el.value; - const flags = `${state.flags.i ? 'i' : ''}${state.flags.m ? 'm' : ''}${state.flags.x ? 'x' : ''}`; - const outputEl = document.getElementById('output'); - const infoEl = document.getElementById('info'); - outputEl.classList.remove('error', 'subclass'); - infoEl.classList.add('hidden'); - const opts = { +function showTranspiled() { + ui.output.classList.remove('error', 'subclass'); + ui.subclassInfo.classList.add('hidden'); + const options = { ...state.opts, - flags, + flags: `${state.flags.i ? 'i' : ''}${state.flags.m ? 'm' : ''}${state.flags.x ? 'x' : ''}`, maxRecursionDepth: state.opts.maxRecursionDepth === '' ? null : +state.opts.maxRecursionDepth, }; - let output = ''; + const errorObj = {error: true}; + let details; + let result = ''; try { - // Use `toDetails` but display output as if `toRegExp` was called. This avoids erroring when - // the selected `target` includes features that don't work in the user's browser - const details = OnigurumaToES.toDetails(input, opts); + // Use `toDetails` but display as if `toRegExp` was called. This avoids erroring when the + // selected `target` includes features that don't work in the user's browser + details = OnigurumaToES.toDetails(ui.input.value, options); if (details.strategy) { - infoEl.classList.remove('hidden'); - outputEl.classList.add('subclass'); - output = getFormattedSubclass(details.pattern, details.flags, details.strategy); + result = getFormattedSubclass(details.pattern, details.flags, details.strategy); + ui.subclassInfo.classList.remove('hidden'); + ui.output.classList.add('subclass'); } else { - output = `/${getRegExpLiteralPattern(details.pattern)}/${details.flags}`; + result = `/${getRegExpLiteralPattern(details.pattern)}/${details.flags}`; } } catch (err) { - outputEl.classList.add('error'); - output = `Error: ${err.message}`; + details = errorObj; + result = `Error: ${err.message}`; + ui.output.classList.add('error'); + } + ui.output.innerHTML = escapeHtml(result); + + // ## Compare to all other accuracy/target combinations + const otherTargetAccuracyCombinations = ['ES2018', 'ES2024', 'ESNext'].flatMap( + t => ['loose', 'default', 'strict'].map(a => ({target: t, accuracy: a})) + ).filter(c => c.target !== options.target || c.accuracy !== options.accuracy); + const differents = []; + // Collect the different results, including differences in error status + for (const other of otherTargetAccuracyCombinations) { + let otherDetails; + try { + otherDetails = OnigurumaToES.toDetails(ui.input.value, {...options, ...other}); + } catch (err) { + otherDetails = errorObj; + } finally { + if (!areDetailsEqual(details, otherDetails)) { + differents.push({ + ...other, + error: !!otherDetails.error, + }); + } + } + } + // Compose and display message about differences or lack thereof + if (differents.length) { + let str = '

🔀'; + const withError = []; + const withDiff = []; + differents.forEach(d => (d.error ? withError : withDiff).push(d)); + if (withError.length) { + str += ` Can't emulate for ${listDifferents(withError)}.`; + } + if (withDiff.length) { + str += ` Emulation uses different details for ${listDifferents(withDiff)}.`; + } + ui.alternateInfo.innerHTML = str; + } else { + ui.alternateInfo.innerHTML = `

🟰 Results are the same ${ + details.error ? '' : '(apart from flag u/v) ' + }with all other targets and accuracies.

`; } - outputEl.innerHTML = escapeHtml(output); } -function autoGrow(el) { - el.style.height = '0'; - el.style.height = (el.scrollHeight + 5) + 'px'; +function areDetailsEqual(a, b) { + if (a.error && b.error) { + return true; + } + if (a.error !== b.error) { + return false; + } + return a.pattern === b.pattern && + a.flags.replace(/[vu]/, '') === b.flags.replace(/[vu]/, '') && + a.strategy?.name === b.strategy?.name && + a.strategy?.subpattern === b.strategy?.subpattern; } function escapeHtml(str) { @@ -76,12 +133,25 @@ function getValue(id) { return el.type === 'checkbox' ? el.checked : el.value; } +function listDifferents(arr) { + const target = {}; + for (const a of arr) { + target[a.target] ?? (target[a.target] = []); + target[a.target].push(a.accuracy); + } + return Object.keys(target).map(t => { + return `target '${t}' with ${ + target[t].length > 1 ? 'accuracies' : 'accuracy' + } '${target[t].join("'/'")}'`; + }).join(', '); +} + function setFlag(flag, value) { state.flags[flag] = value; - showOutput(inputEl); + showTranspiled(); } function setOption(option, value) { state.opts[option] = value; - showOutput(inputEl); + showTranspiled(); } diff --git a/demo/index.html b/demo/index.html index ab0e5b1..4adeec6 100644 --- a/demo/index.html +++ b/demo/index.html @@ -17,7 +17,7 @@

Use this page to test the output of Oniguruma-To-ES, an Oniguruma to JavaScript regex transpiler. See Readme: Supported features.

Try it

-

+

Try it


-    
-    

See Readme: Options for more detailed explanations of each option. The output shows the result of calling toRegExp. Functions toDetails and toOnigurumaAst can also be run from the console on this page. Pretty-print ASTs by passing them to printAst. +

+

+

See Readme: Options for more detailed explanations of each option. The output shows the result of calling toRegExp. Functions toDetails and toOnigurumaAst can also be run from the console on this page. Pretty-print ASTs by passing them to printAst.

diff --git a/spec/match-backreference.spec.js b/spec/match-backreference.spec.js index dfee9de..f8ec0f8 100644 --- a/spec/match-backreference.spec.js +++ b/spec/match-backreference.spec.js @@ -382,7 +382,7 @@ describe('Backreference', () => { target, })).toThrow(); }); - // Matches same case as group with other `accuracy` values + // Matches only the same case as the reffed case-sensitive group with other `accuracy` values ['default', 'loose'].forEach(accuracy => { expect('aa').toExactlyMatch({ pattern: r`(a)(?i)\1`,