Skip to content

Commit

Permalink
ignore / if the delimiter is something else (#1850)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
Fil and mbostock authored Sep 12, 2023
1 parent 72ac2b5 commit a063b22
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 9 deletions.
66 changes: 57 additions & 9 deletions src/transforms/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
80 changes: 80 additions & 0 deletions test/output/treeDelimiter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 80 additions & 0 deletions test/output/treeDelimiter2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/plots/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
50 changes: 50 additions & 0 deletions test/plots/tree-delimiter.ts
Original file line number Diff line number Diff line change
@@ -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
])
]
});
}

0 comments on commit a063b22

Please sign in to comment.