Skip to content

Commit 296e8c4

Browse files
authored
fixes for stratify.path (#187)
1 parent 7ca356e commit 296e8c4

File tree

2 files changed

+146
-23
lines changed

2 files changed

+146
-23
lines changed

src/stratify.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -112,34 +112,34 @@ export default function() {
112112
return stratify;
113113
}
114114

115-
// To normalize a path, we coerce to a string, strip trailing slash if present,
116-
// and add leading slash if missing. This requires counting the number of
117-
// preceding backslashes which may be used to escape the forward slash: an odd
118-
// number indicates an escaped forward slash.
115+
// To normalize a path, we coerce to a string, strip the trailing slash if any
116+
// (as long as the trailing slash is not immediately preceded by another slash),
117+
// and add leading slash if missing.
119118
function normalize(path) {
120119
path = `${path}`;
121-
let i = path.length - 1;
122-
if (path[i] === "/") {
123-
let k = 0;
124-
while (i > 0 && path[--i] === "\\") ++k;
125-
if ((k & 1) === 0) path = path.slice(0, -1);
126-
}
120+
let i = path.length;
121+
if (slash(path, i - 1) && !slash(path, i - 2)) path = path.slice(0, -1);
127122
return path[0] === "/" ? path : `/${path}`;
128123
}
129124

130125
// Walk backwards to find the first slash that is not the leading slash, e.g.:
131126
// "/foo/bar" ⇥ "/foo", "/foo" ⇥ "/", "/" ↦ "". (The root is special-cased
132-
// because the id of the root must be a truthy value.) The slash may be escaped,
133-
// which again requires counting the number of preceding backslashes. Note that
134-
// normalized paths cannot end with a slash except for the root.
127+
// because the id of the root must be a truthy value.)
135128
function parentof(path) {
136129
let i = path.length;
137-
while (i > 2) {
138-
if (path[--i] === "/") {
139-
let j = i, k = 0;
140-
while (j > 0 && path[--j] === "\\") ++k;
141-
if ((k & 1) === 0) break;
142-
}
130+
if (i < 2) return "";
131+
while (--i > 1) if (slash(path, i)) break;
132+
return path.slice(0, i);
133+
}
134+
135+
// Slashes can be escaped; to determine whether a slash is a path delimiter, we
136+
// count the number of preceding backslashes escaping the forward slash: an odd
137+
// number indicates an escaped forward slash.
138+
function slash(path, i) {
139+
if (path[i] === "/") {
140+
let k = 0;
141+
while (i > 0 && path[--i] === "\\") ++k;
142+
if ((k & 1) === 0) return true;
143143
}
144-
return path.slice(0, i < 3 ? i - 1 : i);
144+
return false;
145145
}

test/stratify-test.js

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,129 @@ it("stratify.path(path) returns the root node", () => {
464464
});
465465
});
466466

467+
it("stratify.path(path) correctly handles single-character folders", () => {
468+
const root = stratify().path(d => d.path)([
469+
{path: "/"},
470+
{path: "/d"},
471+
{path: "/d/123"}
472+
]);
473+
assert(root instanceof hierarchy);
474+
assert.deepStrictEqual(noparent(root), {
475+
id: "/",
476+
depth: 0,
477+
height: 2,
478+
data: {path: "/"},
479+
children: [
480+
{
481+
id: "/d",
482+
depth: 1,
483+
height: 1,
484+
data: {path: "/d"},
485+
children: [
486+
{
487+
id: "/d/123",
488+
depth: 2,
489+
height: 0,
490+
data: {path: "/d/123"}
491+
}
492+
]
493+
}
494+
]
495+
});
496+
});
497+
498+
it("stratify.path(path) correctly handles empty folders", () => {
499+
const root = stratify().path(d => d.path)([
500+
{path: "/"},
501+
{path: "//"},
502+
{path: "///"}
503+
]);
504+
assert(root instanceof hierarchy);
505+
assert.deepStrictEqual(noparent(root), {
506+
id: "/",
507+
depth: 0,
508+
height: 2,
509+
data: {path: "/"},
510+
children: [
511+
{
512+
id: "//",
513+
depth: 1,
514+
height: 1,
515+
data: {path: "//"},
516+
children: [
517+
{
518+
id: "///",
519+
depth: 2,
520+
height: 0,
521+
data: {path: "///"}
522+
}
523+
]
524+
}
525+
]
526+
});
527+
});
528+
529+
it("stratify.path(path) correctly handles single-character folders with trailing slashes", () => {
530+
const root = stratify().path(d => d.path)([
531+
{path: "/"},
532+
{path: "/d/"},
533+
{path: "/d/123/"}
534+
]);
535+
assert(root instanceof hierarchy);
536+
assert.deepStrictEqual(noparent(root), {
537+
id: "/",
538+
depth: 0,
539+
height: 2,
540+
data: {path: "/"},
541+
children: [
542+
{
543+
id: "/d",
544+
depth: 1,
545+
height: 1,
546+
data: {path: "/d/"},
547+
children: [
548+
{
549+
id: "/d/123",
550+
depth: 2,
551+
height: 0,
552+
data: {path: "/d/123/"}
553+
}
554+
]
555+
}
556+
]
557+
});
558+
});
559+
560+
it("stratify.path(path) correctly handles imputed single-character folders", () => {
561+
const root = stratify().path(d => d.path)([
562+
{path: "/"},
563+
{path: "/d/123"}
564+
]);
565+
assert(root instanceof hierarchy);
566+
assert.deepStrictEqual(noparent(root), {
567+
id: "/",
568+
depth: 0,
569+
height: 2,
570+
data: {path: "/"},
571+
children: [
572+
{
573+
id: "/d",
574+
depth: 1,
575+
height: 1,
576+
data: null,
577+
children: [
578+
{
579+
id: "/d/123",
580+
depth: 2,
581+
height: 0,
582+
data: {path: "/d/123"}
583+
}
584+
]
585+
}
586+
]
587+
});
588+
});
589+
467590
it("stratify.path(path) allows slashes to be escaped", () => {
468591
const root = stratify().path(d => d.path)([
469592
{path: "/"},
@@ -650,9 +773,9 @@ it("stratify.path(path) implicitly trims trailing slashes", () => {
650773
});
651774
});
652775

653-
it("stratify.path(path) trims at most one trailing slash", () => {
776+
it("stratify.path(path) does not trim trailing slashes preceded by a slash", () => {
654777
const root = stratify().path(d => d.path)([
655-
{path: "/aa///"},
778+
{path: "/aa//"},
656779
{path: "/b"}
657780
]);
658781
assert(root instanceof hierarchy);
@@ -684,7 +807,7 @@ it("stratify.path(path) trims at most one trailing slash", () => {
684807
id: "/aa//",
685808
depth: 3,
686809
height: 0,
687-
data: {path: "/aa///"},
810+
data: {path: "/aa//"},
688811
}
689812
]
690813
}

0 commit comments

Comments
 (0)