Skip to content

Commit 47e7cbe

Browse files
committed
fix(html): correct relative link depth on default-module subpages
Symbol pages are written to `{short_path.path}/~/{name}.html` for every module, including the main entrypoint. But `href_path_resolve` hard-coded the main entrypoint to a directory depth of 1 when computing the `../` prefix for links back to the root. That is only correct when the main module's path is `.` (the usual single-package case). When several entrypoints share a common ancestor, the main module gets a deeper path (e.g. `index.ts`), so its symbol pages live two directories deep yet linked back to the root with a single `../` — producing 404s for stylesheets, search results and cross-links on those subpages. Compute the depth from the actual number of path segments instead, treating `.` as zero segments so the common case is unchanged.
1 parent 64a834e commit 47e7cbe

1 file changed

Lines changed: 101 additions & 9 deletions

File tree

src/html/util.rs

Lines changed: 101 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -377,17 +377,25 @@ pub fn href_path_resolve(
377377
current: UrlResolveKind,
378378
target: UrlResolveKind,
379379
) -> String {
380-
let backs = match current {
381-
UrlResolveKind::File { file } => "../".repeat(if file.is_main {
382-
1
380+
// The number of directories a file's page is nested under, relative to the
381+
// output root. A page lives at `{file.path}/index.html` (or, for a symbol,
382+
// `{file.path}/~/{symbol}.html`), so the depth is the number of segments in
383+
// `file.path`. The main entrypoint is rewritten to `.`, which adds no
384+
// directory, so it counts as zero segments. Previously the main entrypoint
385+
// was hard-coded to a depth of 1; that is only correct when its path is `.`
386+
// and produced broken `../` links whenever the main module had a deeper path
387+
// (e.g. when multiple entrypoints share a common ancestor).
388+
fn path_depth(file: &ShortPath) -> usize {
389+
if file.path == "." {
390+
0
383391
} else {
384392
file.path.split('/').count()
385-
}),
386-
UrlResolveKind::Symbol { file, .. } => "../".repeat(if file.is_main {
387-
1
388-
} else {
389-
file.path.split('/').count() + 1
390-
}),
393+
}
394+
}
395+
396+
let backs = match current {
397+
UrlResolveKind::File { file } => "../".repeat(path_depth(file)),
398+
UrlResolveKind::Symbol { file, .. } => "../".repeat(path_depth(file) + 1),
391399
UrlResolveKind::Root => String::new(),
392400
UrlResolveKind::AllSymbols => String::from("./"),
393401
UrlResolveKind::Category { .. } => String::from("./"),
@@ -955,3 +963,87 @@ pub fn slugify(name: &str) -> String {
955963
.replace_all(&name.to_lowercase(), "")
956964
.replace(' ', "-")
957965
}
966+
967+
#[cfg(test)]
968+
mod tests {
969+
use super::*;
970+
use deno_ast::ModuleSpecifier;
971+
972+
fn short_path(path: &str, is_main: bool) -> ShortPath {
973+
ShortPath {
974+
path: path.to_string(),
975+
specifier: ModuleSpecifier::parse("file:///mod.ts").unwrap(),
976+
is_main,
977+
}
978+
}
979+
980+
#[test]
981+
fn href_path_resolve_main_entrypoint_depth() {
982+
// The main entrypoint rewritten to `.` has no directory of its own, so its
983+
// symbol pages live at `./~/Foo.html` (one level deep) and link back to the
984+
// root with a single `../`.
985+
let main_root = short_path(".", true);
986+
assert_eq!(
987+
href_path_resolve(
988+
UrlResolveKind::Symbol {
989+
file: &main_root,
990+
symbol: "Foo",
991+
},
992+
UrlResolveKind::Root,
993+
),
994+
"../",
995+
);
996+
997+
// When the main entrypoint has a deeper path (e.g. several entrypoints share
998+
// a common ancestor, so the main module is `index.ts`), its symbol page
999+
// lives at `index.ts/~/Foo.html` — two levels deep — and must reach the root
1000+
// with `../../`. Regression test for incorrect `../` links on default-module
1001+
// subpages.
1002+
let main_nested = short_path("index.ts", true);
1003+
assert_eq!(
1004+
href_path_resolve(
1005+
UrlResolveKind::Symbol {
1006+
file: &main_nested,
1007+
symbol: "Foo",
1008+
},
1009+
UrlResolveKind::Root,
1010+
),
1011+
"../../",
1012+
);
1013+
// Linking to the all-symbols page from such a subpage must also climb out.
1014+
assert_eq!(
1015+
href_path_resolve(
1016+
UrlResolveKind::Symbol {
1017+
file: &main_nested,
1018+
symbol: "Foo",
1019+
},
1020+
UrlResolveKind::AllSymbols,
1021+
),
1022+
"../.././all_symbols.html",
1023+
);
1024+
}
1025+
1026+
#[test]
1027+
fn href_path_resolve_non_main_unchanged() {
1028+
let file = short_path("foo", false);
1029+
// index page: `foo/index.html` -> one level deep.
1030+
assert_eq!(
1031+
href_path_resolve(
1032+
UrlResolveKind::File { file: &file },
1033+
UrlResolveKind::Root,
1034+
),
1035+
"../",
1036+
);
1037+
// symbol page: `foo/~/Bar.html` -> two levels deep.
1038+
assert_eq!(
1039+
href_path_resolve(
1040+
UrlResolveKind::Symbol {
1041+
file: &file,
1042+
symbol: "Bar",
1043+
},
1044+
UrlResolveKind::Root,
1045+
),
1046+
"../../",
1047+
);
1048+
}
1049+
}

0 commit comments

Comments
 (0)