Skip to content

Commit a063b22

Browse files
Filmbostock
andauthored
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 <mbostock@gmail.com>
1 parent 72ac2b5 commit a063b22

File tree

5 files changed

+268
-9
lines changed

5 files changed

+268
-9
lines changed

src/transforms/tree.js

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -167,18 +167,66 @@ function nodeData(field) {
167167
}
168168

169169
function normalizer(delimiter = "/") {
170-
return `${delimiter}` === "/"
171-
? (P) => P // paths are already slash-separated
172-
: (P) => P.map(replaceAll(delimiter, "/")); // TODO string.replaceAll when supported
170+
delimiter = `${delimiter}`;
171+
if (delimiter === "/") return (P) => P; // paths are already slash-separated
172+
if (delimiter.length !== 1) throw new Error("delimiter must be exactly one character");
173+
const delimiterCode = delimiter.charCodeAt(0);
174+
return (P) => P.map((p) => slashDelimiter(p, delimiterCode));
173175
}
174176

175-
function replaceAll(search, replace) {
176-
search = new RegExp(regexEscape(search), "g");
177-
return (value) => (value == null ? null : `${value}`.replace(search, replace));
177+
const CODE_BACKSLASH = 92;
178+
const CODE_SLASH = 47;
179+
180+
function slashDelimiter(input, delimiterCode) {
181+
if (delimiterCode === CODE_BACKSLASH) throw new Error("delimiter cannot be backslash");
182+
let afterBackslash = false;
183+
for (let i = 0, n = input.length; i < n; ++i) {
184+
switch (input.charCodeAt(i)) {
185+
case CODE_BACKSLASH:
186+
if (!afterBackslash) {
187+
afterBackslash = true;
188+
continue;
189+
}
190+
break;
191+
case delimiterCode:
192+
if (afterBackslash) {
193+
(input = input.slice(0, i - 1) + input.slice(i)), --i, --n; // remove backslash
194+
} else {
195+
input = input.slice(0, i) + "/" + input.slice(i + 1); // replace delimiter with slash
196+
}
197+
break;
198+
case CODE_SLASH:
199+
if (afterBackslash) {
200+
(input = input.slice(0, i) + "\\\\" + input.slice(i)), (i += 2), (n += 2); // add two backslashes
201+
} else {
202+
(input = input.slice(0, i) + "\\" + input.slice(i)), ++i, ++n; // add backslash
203+
}
204+
break;
205+
}
206+
afterBackslash = false;
207+
}
208+
return input;
178209
}
179210

180-
function regexEscape(string) {
181-
return `${string}`.replace(/[\\^$*+?.()|[\]{}]/g, "\\$&");
211+
function slashUnescape(input) {
212+
let afterBackslash = false;
213+
for (let i = 0, n = input.length; i < n; ++i) {
214+
switch (input.charCodeAt(i)) {
215+
case CODE_BACKSLASH:
216+
if (!afterBackslash) {
217+
afterBackslash = true;
218+
continue;
219+
}
220+
// eslint-disable-next-line no-fallthrough
221+
case CODE_SLASH:
222+
if (afterBackslash) {
223+
(input = input.slice(0, i - 1) + input.slice(i)), --i, --n; // remove backslash
224+
}
225+
break;
226+
}
227+
afterBackslash = false;
228+
}
229+
return input;
182230
}
183231

184232
function isNodeValue(option) {
@@ -272,7 +320,7 @@ function parentValue(evaluate) {
272320
function nameof(path) {
273321
let i = path.length;
274322
while (--i > 0) if (slash(path, i)) break;
275-
return path.slice(i + 1);
323+
return slashUnescape(path.slice(i + 1));
276324
}
277325

278326
// Slashes can be escaped; to determine whether a slash is a path delimiter, we

test/output/treeDelimiter.svg

Lines changed: 80 additions & 0 deletions
Loading

test/output/treeDelimiter2.svg

Lines changed: 80 additions & 0 deletions
Loading

test/plots/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ export * from "./title.js";
298298
export * from "./traffic-horizon.js";
299299
export * from "./travelers-covid-drop.js";
300300
export * from "./travelers-year-over-year.js";
301+
export * from "./tree-delimiter.js";
301302
export * from "./uniform-random-difference.js";
302303
export * from "./untyped-date-bin.js";
303304
export * from "./us-congress-age-color-explicit.js";

test/plots/tree-delimiter.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as Plot from "@observablehq/plot";
2+
3+
export async function treeDelimiter() {
4+
return Plot.plot({
5+
axis: null,
6+
height: 150,
7+
margin: 10,
8+
marginLeft: 40,
9+
marginRight: 190,
10+
marks: [
11+
Plot.tree(
12+
[
13+
"foo;a;//example", // foo → a → //example
14+
"foo;a;//example/1", // foo → a → //example/1
15+
"foo;b;//example/2", // foo → b → //example/2
16+
"foo;c\\;c;//example2", // foo → c;c → //example2
17+
"foo;d\\\\;d;//example2", // foo → d\ → d → //example3
18+
"foo;d\\\\;\\d;//example2", // foo → d\ → \d → //example3
19+
"foo;e\\\\\\;e;//example2", // foo → e\;e → //example3
20+
"foo;f/f;//example4", // foo → f/f → //example4
21+
"foo;g\\/g;//example3" // foo → g\/g → //example3
22+
],
23+
{delimiter: ";"}
24+
)
25+
]
26+
});
27+
}
28+
29+
export async function treeDelimiter2() {
30+
return Plot.plot({
31+
axis: null,
32+
height: 150,
33+
margin: 10,
34+
marginLeft: 40,
35+
marginRight: 190,
36+
marks: [
37+
Plot.tree([
38+
"foo/a/\\/\\/example", // foo → a → //example
39+
"foo/a/\\/\\/example\\/1", // foo → a → //example/1
40+
"foo/b/\\/\\/example\\/2", // foo → b → //example/2
41+
"foo/c;c/\\/\\/example2", // foo → c;c → //example2
42+
"foo/d\\\\/d/\\/\\/example2", // foo → d\ → d → //example3
43+
"foo/d\\\\/\\d/\\/\\/example2", // foo → d\ → \d → //example3
44+
"foo/e\\\\;e/\\/\\/example2", // foo → e\;e → //example3
45+
"foo/f\\/f/\\/\\/example4", // foo → f/f → //example4
46+
"foo/g\\\\\\/g/\\/\\/example3" // foo → g\/g → //example3
47+
])
48+
]
49+
});
50+
}

0 commit comments

Comments
 (0)