Skip to content

Commit

Permalink
fix: TOC box should contain only the TOC element content
Browse files Browse the repository at this point in the history
This improves TOC handling in web publications and EPUBs

- fix #1258
  • Loading branch information
MurakamiShinyu committed Feb 3, 2024
1 parent 0bbbd48 commit 754a70c
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 91 deletions.
60 changes: 10 additions & 50 deletions packages/core/src/vivliostyle/assets.ts
Expand Up @@ -1371,69 +1371,29 @@ ncx|content {
display: none;
}
body > * {
-adapt-behavior: body-child;
*:not([role=doc-toc],
[role=doc-toc] *,
:has([role=doc-toc]),
:is(h1,h2,h3,h4,h5,h6):has(+:not(nav)[role=doc-toc])) {
display: none;
}
[role="doc-toc"],
[role="directory"],
nav,
.toc,
#toc,
section:has(>:first-child:is(h1,h2,h3,h4,h5,h6):is(.toc,#toc)),
#table-of-contents,
#contents {
-adapt-behavior: toc-root;
}
[role="doc-toc"] a,
[role="directory"] a,
nav a,
.toc a,
#toc a,
section:has(>:first-child:is(h1,h2,h3,h4,h5,h6):is(.toc,#toc)) a,
[role=doc-toc] li a,
ncx|navLabel {
-adapt-behavior: toc-node-anchor;
}
[role="doc-toc"] li,
[role="directory"] li,
nav li,
.toc li,
#toc li,
section:has(>:first-child:is(h1,h2,h3,h4,h5,h6):is(.toc,#toc)) li,
[role=doc-toc] li,
ncx|navPoint {
-adapt-behavior: toc-node;
}
[role="doc-toc"] li > *:first-child,
[role="directory"] li > *:first-child,
nav li > *:first-child,
.toc li > *:first-child,
#toc li > *:first-child,
section:has(>:first-child:is(h1,h2,h3,h4,h5,h6):is(.toc,#toc)) li > *:first-child {
[role=doc-toc] li > :not(ul,ol):first-child {
-adapt-behavior: toc-node-first-child;
}
[role="doc-toc"] ol,
[role="directory"] ol,
nav ol,
.toc ol,
#toc ol,
[role="doc-toc"] ul,
[role="directory"] ul,
nav ul,
.toc ul,
#toc ul,
ol[role="doc-toc"],
ol[role="directory"],
ol.toc,
ol#toc,
ul[role="doc-toc"],
ul[role="directory"],
ul.toc,
ul#toc,
section:has(>:first-child:is(h1,h2,h3,h4,h5,h6):is(.toc,#toc)) :is(ol,ul) {
[role=doc-toc] :is(ol,ul),
[role=doc-toc]:is(ol,ul) {
-adapt-behavior: toc-container;
}
`;
Expand Down
28 changes: 7 additions & 21 deletions packages/core/src/vivliostyle/epub.ts
Expand Up @@ -247,9 +247,6 @@ export class EPUBDocStore extends OPS.OPSDocStore {
const doc = xmldoc.document;
const opf = new OPFDoc(this, url);

if (doc.body) {
doc.body.setAttribute("data-vivliostyle-primary-entry", true);
}
// Find manifest, W3C WebPublication or Readium Web Publication Manifest
const manifestLink = doc.querySelector(
"link[rel='publication'],link[rel='manifest'][type='application/webpub+json']",
Expand Down Expand Up @@ -284,13 +281,9 @@ export class EPUBDocStore extends OPS.OPSDocStore {
// No manifest
opf.initWithWebPubManifest({}, doc).then(() => {
if (opf.xhtmlToc && opf.xhtmlToc.src === xmldoc.url) {
// xhtmlToc is the primari entry (X)HTML
if (
!doc.querySelector(
"[role=doc-toc], [role=directory], nav, .toc, #toc",
)
) {
// TOC is not found in the primari entry (X)HTML
// xhtmlToc is the primary entry (X)HTML
if (Toc.findTocElements(doc).length === 0) {
// TOC is not found in the primary entry (X)HTML
opf.xhtmlToc = null;
}
}
Expand Down Expand Up @@ -1155,16 +1148,7 @@ export class OPFDoc {
manifestObj["readingOrder"] = [encodeURI(primaryEntryPath)];

// Find TOC in the primary entry (X)HTML
const selector =
"[role=doc-toc] a[href]," +
"[role=directory] a[href]," +
"nav li a[href]," +
".toc a[href]," +
"#toc a[href]" +
(CSS.supports("selector(:has(*))")
? ",section:has(>:first-child:is(h1,h2,h3,h4,h5,h6):is(.toc,#toc)) a[href]"
: "");
for (const anchorElem of doc.querySelectorAll(selector)) {
for (const anchorElem of Toc.findTocAnchorElements(doc)) {
const href = anchorElem.getAttribute("href");
if (/^(https?:)?\/\//.test(href)) {
// Avoid link to external resources
Expand Down Expand Up @@ -2642,7 +2626,9 @@ export class OPFView implements Vgen.CustomRendererFactory {
const pageCont = viewport.document.createElement("div") as HTMLElement;
viewport.root.appendChild(pageCont);
// pageCont.style.position = "absolute";
pageCont.style.visibility = "hidden";
if (!Constants.isDebug) {
pageCont.style.visibility = "hidden";
}
// pageCont.style.left = "3px";
// pageCont.style.top = "3px";
pageCont.style.width = `${tocWidth + 10}px`;
Expand Down
71 changes: 51 additions & 20 deletions packages/core/src/vivliostyle/toc.ts
Expand Up @@ -44,6 +44,52 @@ export type TOCItem = {
children: TOCItem[];
};

export function findTocElements(doc: Document): Array<Element> {
// Find `role="doc-toc"` for webpub or `<nav epub:type="toc">` etc. for EPUB.
const tocElems = Array.from(
doc.querySelectorAll(
"[role=doc-toc],nav[*|type=toc],nav[*|type=landmarks],nav[*|type=page-list]",
),
);
if (tocElems.length > 0) {
return tocElems;
}
// If not found, find TOC elements with the following selector.
const navs = "nav,.toc,#toc,#table-of-contents,#contents,[role=directory]";
for (const elem of doc.querySelectorAll(navs)) {
if (tocElems.find((e) => e.contains(elem))) {
continue; // Skip nested TOC elements.
}
let tocElem = elem;
if (/^h[1-6]$/.test(tocElem.localName)) {
// If the element is a heading, use its parent or next sibling as TOC element.
if (!tocElem.previousElementSibling) {
// If the heading is the first element, use its parent.
tocElem = tocElem.parentElement;
} else {
// Otherwise, use its next sibling.
tocElem = tocElem.nextElementSibling;
}
}
// TOC element must have at least one list item with an anchor element.
if (tocElem && tocElem.querySelector("li a[href]")) {
tocElems.push(tocElem);
}
}
return tocElems;
}

export function findTocAnchorElements(doc: Document): Array<Element> {
const tocElems = findTocElements(doc);
const anchors = [] as Array<Element>;
for (const tocElem of tocElems) {
for (const anchor of tocElem.querySelectorAll("li a[href]")) {
anchors.push(anchor);
}
}
return anchors;
}

export class TOCView implements Vgen.CustomRendererFactory {
pref: Exprs.Preferences;
page: Vtree.Page = null;
Expand Down Expand Up @@ -95,16 +141,6 @@ export class TOCView implements Vgen.CustomRendererFactory {
const behavior = computedStyle["behavior"];
if (behavior) {
switch (behavior.toString()) {
case "body-child":
if (
!srcElem.querySelector(
"[role=doc-toc], [role=directory], nav li a, .toc, #toc",
)
) {
// hide elements not containing TOC.
computedStyle["display"] = Css.ident.none;
}
break;
case "toc-node-anchor":
computedStyle["color"] = Css.ident.inherit;
computedStyle["text-decoration"] = Css.ident.none;
Expand Down Expand Up @@ -230,16 +266,11 @@ export class TOCView implements Vgen.CustomRendererFactory {
const tocBoxUrl = Base.stripFragment(this.url) + "?viv-toc-box";

this.store.load(tocBoxUrl).then((xmldoc) => {
// Mark if this doc is the primary entry page.
const nonTocBoxDoc = this.store.resources[this.url];

// Make hidden TOC visible in TOC box
for (const elem of xmldoc.document.querySelectorAll(
"[role=doc-toc], [role=directory], nav, .toc, #toc",
)) {
if (elem.hasAttribute("hidden")) {
elem.removeAttribute("hidden");
}
for (const tocElem of findTocElements(xmldoc.document)) {
// Set `role="doc-toc"`
tocElem.setAttribute("role", "doc-toc");
// Make hidden TOC visible in TOC box
tocElem.removeAttribute("hidden");
}

const style = this.store.getStyleForDoc(xmldoc);
Expand Down

0 comments on commit 754a70c

Please sign in to comment.