From d99190a93b6d588fe8690de6ff406dd19cfb7d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 13 Sep 2023 00:50:32 +0200 Subject: [PATCH] ignore / if the delimiter is something else (#1850) * ignore / if the delimiter is something else (note: I don't think this is the correct fix! But at least it gives a unit test) closes #1849 * avoid string.replaceAll * add delimiter test * \/ needs three (six) backslashes! * pass tests, but still not complete * proper escape/unescape * refactor logic * lift delimiter code * tweak error message * refactor logic slightly --------- Co-authored-by: Mike Bostock --- src/transforms/tree.js | 66 ++++++++++++++++++++++++---- test/output/treeDelimiter.svg | 80 ++++++++++++++++++++++++++++++++++ test/output/treeDelimiter2.svg | 80 ++++++++++++++++++++++++++++++++++ test/plots/index.ts | 1 + test/plots/tree-delimiter.ts | 50 +++++++++++++++++++++ 5 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 test/output/treeDelimiter.svg create mode 100644 test/output/treeDelimiter2.svg create mode 100644 test/plots/tree-delimiter.ts diff --git a/src/transforms/tree.js b/src/transforms/tree.js index 44d42cf474..067fc03229 100644 --- a/src/transforms/tree.js +++ b/src/transforms/tree.js @@ -167,18 +167,66 @@ function nodeData(field) { } function normalizer(delimiter = "/") { - return `${delimiter}` === "/" - ? (P) => P // paths are already slash-separated - : (P) => P.map(replaceAll(delimiter, "/")); // TODO string.replaceAll when supported + delimiter = `${delimiter}`; + if (delimiter === "/") return (P) => P; // paths are already slash-separated + if (delimiter.length !== 1) throw new Error("delimiter must be exactly one character"); + const delimiterCode = delimiter.charCodeAt(0); + return (P) => P.map((p) => slashDelimiter(p, delimiterCode)); } -function replaceAll(search, replace) { - search = new RegExp(regexEscape(search), "g"); - return (value) => (value == null ? null : `${value}`.replace(search, replace)); +const CODE_BACKSLASH = 92; +const CODE_SLASH = 47; + +function slashDelimiter(input, delimiterCode) { + if (delimiterCode === CODE_BACKSLASH) throw new Error("delimiter cannot be backslash"); + let afterBackslash = false; + for (let i = 0, n = input.length; i < n; ++i) { + switch (input.charCodeAt(i)) { + case CODE_BACKSLASH: + if (!afterBackslash) { + afterBackslash = true; + continue; + } + break; + case delimiterCode: + if (afterBackslash) { + (input = input.slice(0, i - 1) + input.slice(i)), --i, --n; // remove backslash + } else { + input = input.slice(0, i) + "/" + input.slice(i + 1); // replace delimiter with slash + } + break; + case CODE_SLASH: + if (afterBackslash) { + (input = input.slice(0, i) + "\\\\" + input.slice(i)), (i += 2), (n += 2); // add two backslashes + } else { + (input = input.slice(0, i) + "\\" + input.slice(i)), ++i, ++n; // add backslash + } + break; + } + afterBackslash = false; + } + return input; } -function regexEscape(string) { - return `${string}`.replace(/[\\^$*+?.()|[\]{}]/g, "\\$&"); +function slashUnescape(input) { + let afterBackslash = false; + for (let i = 0, n = input.length; i < n; ++i) { + switch (input.charCodeAt(i)) { + case CODE_BACKSLASH: + if (!afterBackslash) { + afterBackslash = true; + continue; + } + // eslint-disable-next-line no-fallthrough + case CODE_SLASH: + if (afterBackslash) { + (input = input.slice(0, i - 1) + input.slice(i)), --i, --n; // remove backslash + } + break; + } + afterBackslash = false; + } + return input; } function isNodeValue(option) { @@ -272,7 +320,7 @@ function parentValue(evaluate) { function nameof(path) { let i = path.length; while (--i > 0) if (slash(path, i)) break; - return path.slice(i + 1); + return slashUnescape(path.slice(i + 1)); } // Slashes can be escaped; to determine whether a slash is a path delimiter, we diff --git a/test/output/treeDelimiter.svg b/test/output/treeDelimiter.svg new file mode 100644 index 0000000000..f806f3cc9d --- /dev/null +++ b/test/output/treeDelimiter.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + /foo + /foo/a + /foo/b + /foo/c;c + /foo/e\\;e + /foo/f\/f + /foo/g\\\/g + /foo/d\\ + /foo/a/\/\/example + /foo/a/\/\/example\/1 + /foo/b/\/\/example\/2 + /foo/c;c/\/\/example2 + /foo/e\\;e/\/\/example2 + /foo/f\/f/\/\/example4 + /foo/g\\\/g/\/\/example3 + /foo/d\\/d + /foo/d\\/\d + /foo/d\\/d/\/\/example2 + /foo/d\\/\d/\/\/example2 + + + //example/foo/a/\/\/example + //example/1/foo/a/\/\/example\/1 + //example/2/foo/b/\/\/example\/2 + //example2/foo/c;c/\/\/example2 + //example2/foo/e\\;e/\/\/example2 + //example4/foo/f\/f/\/\/example4 + //example3/foo/g\\\/g/\/\/example3 + //example2/foo/d\\/d/\/\/example2 + //example2/foo/d\\/\d/\/\/example2 + + + foo/foo + a/foo/a + b/foo/b + c;c/foo/c;c + e\;e/foo/e\\;e + f/f/foo/f\/f + g\/g/foo/g\\\/g + d\/foo/d\\ + d/foo/d\\/d + \d/foo/d\\/\d + + \ No newline at end of file diff --git a/test/output/treeDelimiter2.svg b/test/output/treeDelimiter2.svg new file mode 100644 index 0000000000..f806f3cc9d --- /dev/null +++ b/test/output/treeDelimiter2.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + /foo + /foo/a + /foo/b + /foo/c;c + /foo/e\\;e + /foo/f\/f + /foo/g\\\/g + /foo/d\\ + /foo/a/\/\/example + /foo/a/\/\/example\/1 + /foo/b/\/\/example\/2 + /foo/c;c/\/\/example2 + /foo/e\\;e/\/\/example2 + /foo/f\/f/\/\/example4 + /foo/g\\\/g/\/\/example3 + /foo/d\\/d + /foo/d\\/\d + /foo/d\\/d/\/\/example2 + /foo/d\\/\d/\/\/example2 + + + //example/foo/a/\/\/example + //example/1/foo/a/\/\/example\/1 + //example/2/foo/b/\/\/example\/2 + //example2/foo/c;c/\/\/example2 + //example2/foo/e\\;e/\/\/example2 + //example4/foo/f\/f/\/\/example4 + //example3/foo/g\\\/g/\/\/example3 + //example2/foo/d\\/d/\/\/example2 + //example2/foo/d\\/\d/\/\/example2 + + + foo/foo + a/foo/a + b/foo/b + c;c/foo/c;c + e\;e/foo/e\\;e + f/f/foo/f\/f + g\/g/foo/g\\\/g + d\/foo/d\\ + d/foo/d\\/d + \d/foo/d\\/\d + + \ No newline at end of file diff --git a/test/plots/index.ts b/test/plots/index.ts index 35ded36808..9998e92373 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -298,6 +298,7 @@ export * from "./title.js"; export * from "./traffic-horizon.js"; export * from "./travelers-covid-drop.js"; export * from "./travelers-year-over-year.js"; +export * from "./tree-delimiter.js"; export * from "./uniform-random-difference.js"; export * from "./untyped-date-bin.js"; export * from "./us-congress-age-color-explicit.js"; diff --git a/test/plots/tree-delimiter.ts b/test/plots/tree-delimiter.ts new file mode 100644 index 0000000000..f9492c2005 --- /dev/null +++ b/test/plots/tree-delimiter.ts @@ -0,0 +1,50 @@ +import * as Plot from "@observablehq/plot"; + +export async function treeDelimiter() { + return Plot.plot({ + axis: null, + height: 150, + margin: 10, + marginLeft: 40, + marginRight: 190, + marks: [ + Plot.tree( + [ + "foo;a;//example", // foo → a → //example + "foo;a;//example/1", // foo → a → //example/1 + "foo;b;//example/2", // foo → b → //example/2 + "foo;c\\;c;//example2", // foo → c;c → //example2 + "foo;d\\\\;d;//example2", // foo → d\ → d → //example3 + "foo;d\\\\;\\d;//example2", // foo → d\ → \d → //example3 + "foo;e\\\\\\;e;//example2", // foo → e\;e → //example3 + "foo;f/f;//example4", // foo → f/f → //example4 + "foo;g\\/g;//example3" // foo → g\/g → //example3 + ], + {delimiter: ";"} + ) + ] + }); +} + +export async function treeDelimiter2() { + return Plot.plot({ + axis: null, + height: 150, + margin: 10, + marginLeft: 40, + marginRight: 190, + marks: [ + Plot.tree([ + "foo/a/\\/\\/example", // foo → a → //example + "foo/a/\\/\\/example\\/1", // foo → a → //example/1 + "foo/b/\\/\\/example\\/2", // foo → b → //example/2 + "foo/c;c/\\/\\/example2", // foo → c;c → //example2 + "foo/d\\\\/d/\\/\\/example2", // foo → d\ → d → //example3 + "foo/d\\\\/\\d/\\/\\/example2", // foo → d\ → \d → //example3 + "foo/e\\\\;e/\\/\\/example2", // foo → e\;e → //example3 + "foo/f\\/f/\\/\\/example4", // foo → f/f → //example4 + "foo/g\\\\\\/g/\\/\\/example3" // foo → g\/g → //example3 + ]) + ] + }); +}