(function () { "use strict"; // This fixed program runs only in Qt's isolated ApplicationWorld. It adds // no DOM nodes, IDs, bridges, document scripts, or host capabilities. if (globalThis.__docviewReader) return; const MAX_INDEX = 2000; let headings = []; let options = { fixed: false, rtl: false, publisherStyle: true }; let originalTypography = null; let focusedLink = null; let linkOutline = null; let restoredRange = null; let layoutAnchor = null, resourceRestoreFrame = 0; let userScrollPending = false, userScrollFrame = 0; let navigationTargets = [], navigationRevision = ""; let resourceIssues = Object.create(null); // Only aggregate directive classes. Never copy blockedURI, sourceFile, // originalPolicy or sample into the application or its saved state. document.addEventListener("securitypolicyviolation", event => { if (!event.isTrusted || event.disposition !== "enforce") return; const directive = String(event.effectiveDirective || "").split("-")[0]; const category = ({img:"image", style:"style", font:"font", script:"script", frame:"frame", child:"frame", connect:"connect", form:"form", base:"base", object:"object", media:"media"})[directive] || "other"; const key = "csp." + category; resourceIssues[key] = Math.min(999999, (resourceIssues[key] || 0) + 1); }); function text(node, limit = 512) { return String(node ? node.textContent || "" : "").replace(/\s+/g, " ").trim().slice(0, limit); } function visible(element) { if (!element || !element.isConnected || element.hidden || element.closest("[hidden],[aria-hidden='true']")) return false; const style = getComputedStyle(element); return style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse" && element.getClientRects().length > 0; } function axis() { const style = getComputedStyle(document.body || document.documentElement); const vertical = /^(vertical|sideways)/.test(style.writingMode); return { vertical, reverse: vertical && /-rl$/.test(style.writingMode), writingMode: style.writingMode }; } function progression() { const root = document.scrollingElement || document.documentElement; const a = axis(); const total = a.vertical ? root.scrollWidth - innerWidth : root.scrollHeight - innerHeight; const position = a.vertical ? (a.reverse ? -scrollX : scrollX) : scrollY; return total > 0 ? Math.max(0, Math.min(1, position / total)) : 0; } function setProgression(value) { const root = document.scrollingElement || document.documentElement; const a = axis(); const p = Math.max(0, Math.min(1, Number(value) || 0)); const total = a.vertical ? root.scrollWidth - innerWidth : root.scrollHeight - innerHeight; if (a.vertical) scrollTo({ left: Math.max(0, total) * p * (a.reverse ? -1 : 1), top: 0, behavior: "instant" }); else scrollTo({ left: 0, top: Math.max(0, total) * p, behavior: "instant" }); } function hrefFor(reference, base = document.baseURI) { try { const url = new URL(reference, base); if (url.protocol !== "doc:" || url.host !== location.host || url.username || url.password) return ""; return url.pathname.replace(/^\//, "") + url.search + url.hash; } catch (_) { return ""; } } function headingIndex() { headings = []; const nodes = document.querySelectorAll("h1,h2,h3,h4,h5,h6,[role='heading']"); let truncated = false; for (const node of nodes) { if (!visible(node)) continue; const name = node.localName.toLowerCase(); const nativeLevel = /^h[1-6]$/.test(name) ? Number(name[1]) : 0; const ariaLevel = Number(node.getAttribute("aria-level")); const level = nativeLevel || (Number.isInteger(ariaLevel) && ariaLevel > 0 && ariaLevel <= 6 ? ariaLevel : 0); if (!level) continue; if (headings.length === MAX_INDEX) { truncated = true; break; } headings.push({ element: node, level, title: text(node) || "見出し(レベル" + level + ")" }); } return { truncated, rows: headings.map((h, index) => ({ index, title: h.title, level: h.level, depth: h.level - 1, href: h.element.id ? hrefFor("#" + encodeURIComponent(h.element.id), location.href) : "", source: "html-heading", precision: "exact" })) }; } function index() { const result = headingIndex(); const outline = []; const navs = document.querySelectorAll("[role='doc-toc'],nav"); for (const nav of navs) { // XHTML keeps epub:type in its XML namespace; HTML retains the // prefixed attribute name. CSS attribute selectors do not match both. const epubType = nav.getAttributeNS("http://www.idpf.org/2007/ops", "type") || nav.getAttribute("epub:type") || ""; if (nav.getAttribute("role") !== "doc-toc" && !epubType.split(/\s+/).includes("toc")) continue; for (const link of nav.querySelectorAll("a[href]")) { if (!visible(link) || outline.length >= MAX_INDEX) continue; let depth = 0; for (let parent = link.parentElement; parent && parent !== nav; parent = parent.parentElement) if (parent.localName.toLowerCase() === "ol" || parent.localName.toLowerCase() === "ul") ++depth; const href = hrefFor(link.getAttribute("href")); outline.push({ title: text(link) || "目次項目", href, depth: Math.max(0, depth - 1), missing: !href, source: "html-toc" }); } } return { title: String(document.title || "").slice(0, 4096), headings: result.rows, outline, truncated: result.truncated, scriptCount: document.scripts.length, fixedViewport: viewport(), writingMode: axis().writingMode }; } function viewport() { const element = document.querySelector("meta[name='viewport']"); const value = element ? element.getAttribute("content") || "" : ""; const w = /(?:^|[,;\s])width\s*=\s*([0-9.]+)/i.exec(value); const h = /(?:^|[,;\s])height\s*=\s*([0-9.]+)/i.exec(value); let width = w ? Number(w[1]) : 0, height = h ? Number(h[1]) : 0; if ((!width || !height) && document.documentElement.localName === "svg") { const box = document.documentElement.viewBox && document.documentElement.viewBox.baseVal; if (box) { width = box.width; height = box.height; } } return width > 0 && height > 0 && width <= 100000 && height <= 100000 ? { width, height } : {}; } function cfiFor(node, offset = 0) { const steps = []; let target = node; if (!target) return ""; if (target.nodeType === Node.TEXT_NODE) { let elements = 0, previousText = 0; for (const sibling of target.parentNode.childNodes) { if (sibling === target) break; if (sibling.nodeType === Node.ELEMENT_NODE) { ++elements; previousText = 0; } if (sibling.nodeType === Node.TEXT_NODE) previousText += sibling.length; } steps.unshift(elements * 2 + 1); offset += previousText; target = target.parentElement; } while (target && target !== document.documentElement && steps.length < 128) { let order = 1; for (let sibling = target.previousElementSibling; sibling; sibling = sibling.previousElementSibling) ++order; steps.unshift(order * 2); target = target.parentElement; } return target === document.documentElement ? "epubcfi(/" + steps.join("/") + ":" + Math.max(0, offset) + ")" : ""; } function resolveCfi(cfi) { if (typeof cfi !== "string" || cfi.length > 4096 || !/^epubcfi\(.*\)$/.test(cfi)) return null; let value = cfi.slice(8, -1); if (value.includes("!")) value = value.slice(value.lastIndexOf("!") + 1); value = value.replace(/\[(?:\^.|[^\]])*\]/g, ""); if (!/^\/(?:[1-9][0-9]*\/)*[1-9][0-9]*(?::[0-9]+)?$/.test(value)) return null; const parts = value.split(":"); const steps = parts[0].slice(1).split("/").map(Number); if (steps.length > 128) return null; let node = document.documentElement; let offset = Number(parts[1] || 0); for (let i = 0; i < steps.length; ++i) { const step = steps[i]; if (step % 2 === 0) node = node && node.children && node.children[step / 2 - 1]; else { if (!node || i !== steps.length - 1) return null; const slot = (step - 1) / 2; let elements = 0, found = null; for (const child of node.childNodes) { if (child.nodeType === Node.ELEMENT_NODE) ++elements; else if (child.nodeType === Node.TEXT_NODE && elements === slot) { if (offset <= child.length) { found = child; break; } offset -= child.length; } } node = found; } if (!node) return null; } const range = document.createRange(); if (node.nodeType === Node.TEXT_NODE) range.setStart(node, Math.min(offset, node.length)); else range.setStart(node, Math.min(offset, node.childNodes.length)); range.collapse(true); return range; } function visibleRange() { const a = axis(); const x = Math.max(1, Math.min(innerWidth - 1, a.vertical && a.reverse ? innerWidth - 20 : 20)); const y = Math.max(1, Math.min(innerHeight - 1, 20)); let range = document.caretRangeFromPoint ? document.caretRangeFromPoint(x, y) : null; if (range && range.startContainer.nodeType === Node.TEXT_NODE) return range; const element = document.elementFromPoint(x, y) || document.body || document.documentElement; const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); let node, inspected = 0; while ((node = walker.nextNode()) && ++inspected < 1000) { if (!node.textContent.trim() || !visible(node.parentElement)) continue; range = document.createRange(); range.setStart(node, 0); range.collapse(true); return range; } range = document.createRange(); range.selectNodeContents(element); range.collapse(true); return range; } function locate() { // A font can change layout before its loadingdone event, and a location // poll can run before the image's animation-frame restoration. Retain // the pre-layout anchor until that restoration has used it. if (layoutAnchor && layoutMoved(layoutAnchor)) resourcesSettled(); // Reflow can put a different character at the viewport sampling point. // Keep the restored logical character until the reader actually scrolls. const keep = restoredRange && document.documentElement.contains(restoredRange.range.startContainer) && Math.abs(scrollX - restoredRange.x) < 1 && Math.abs(scrollY - restoredRange.y) < 1; if (!keep) restoredRange = null; const range = keep ? restoredRange.range : visibleRange(); const node = range.startContainer; const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement; const rect = range.getBoundingClientRect(); const result = { url: location.href, cfi: cfiFor(node, range.startOffset), fragment: element && element.id || "", progression: progression(), textQuote: node.nodeType === Node.TEXT_NODE ? node.textContent.slice(range.startOffset, range.startOffset + 160) : text(element, 160), offsetX: rect.left / Math.max(1, innerWidth), offsetY: rect.top / Math.max(1, innerHeight), writingMode: axis().writingMode, currentOutline: currentNavigation(range), navigationRevision }; if (!resourceRestoreFrame) layoutAnchor = { range: range.cloneRange(), saved: result, x:scrollX, y:scrollY, documentX:rect.left + scrollX, documentY:rect.top + scrollY, width:innerWidth, height:innerHeight }; return result; } function layoutMoved(anchor) { if (!anchor.range.startContainer.isConnected || anchor.width !== innerWidth || anchor.height !== innerHeight) return false; const rect = anchor.range.getBoundingClientRect(); return Math.abs(rect.left + scrollX - anchor.documentX) > 1 || Math.abs(rect.top + scrollY - anchor.documentY) > 1; } function cancelResourceRestore() { if (resourceRestoreFrame) cancelAnimationFrame(resourceRestoreFrame); resourceRestoreFrame = 0; layoutAnchor = null; } function resourcesSettled() { if (resourceRestoreFrame || !layoutAnchor || options.fixed) return; const anchor = layoutAnchor; resourceRestoreFrame = requestAnimationFrame(() => { resourceRestoreFrame = 0; // Retain the character and its viewport fraction across a decoded // image or font swap. Never insert helper nodes into the source DOM. // Clear the old snapshot before locate(), otherwise it would see // the same completed layout change and schedule another frame. const current = layoutAnchor === anchor; if (current) layoutAnchor = null; if (current && anchor.range.startContainer.isConnected) positionRange(anchor.range, anchor.saved); userScrollPending = false; locate(); }); } document.addEventListener("load", event => { if (event.isTrusted && event.target instanceof HTMLImageElement) resourcesSettled(); }, true); document.addEventListener("error", event => { if (event.isTrusted && event.target instanceof HTMLImageElement) resourcesSettled(); }, true); if (document.fonts) { document.fonts.addEventListener("loadingdone", resourcesSettled); document.fonts.addEventListener("loadingerror", resourcesSettled); } for (const type of ["wheel", "touchstart", "touchmove", "pointerdown", "pointermove", "keydown"]) document.addEventListener(type, event => { if (!event.isTrusted || (type === "pointermove" && !event.buttons)) return; cancelResourceRestore(); // Clicking without scrolling must not leave a 400 ms gap without // any anchor. A following input scroll still takes precedence. if (document.body) locate(); userScrollPending = true; // Input intent applies to this frame, not to an unrelated native // anchor adjustment long after a click that did not scroll. if (userScrollFrame) cancelAnimationFrame(userScrollFrame); userScrollFrame = requestAnimationFrame(() => { userScrollFrame = 0; userScrollPending = false; }); }, {capture:true, passive:true}); document.addEventListener("scroll", () => { if (layoutAnchor && Math.abs(scrollX - layoutAnchor.x) < 1 && Math.abs(scrollY - layoutAnchor.y) < 1) return; if (!userScrollPending && layoutAnchor && layoutMoved(layoutAnchor)) { // Native overflow-anchor can move the scroll offset as resources // reflow. This is not a new reader navigation. resourcesSettled(); return; } userScrollPending = false; cancelResourceRestore(); restoredRange = null; locate(); }, {passive:true}); window.addEventListener("hashchange", () => { cancelResourceRestore(); locate(); }); function configureNavigation(args) { navigationTargets = []; navigationRevision = ""; if (!args || typeof args.revision !== "string" || !/^[0-9]{1,19}$/.test(args.revision) || !Array.isArray(args.targets) || args.targets.length > 20000) return false; const targets = []; for (const item of args.targets) { if (!item || !Number.isInteger(item.row) || item.row < 0 || item.row >= 20000) return false; let element; if (Number.isInteger(item.heading) && item.heading >= 0 && item.heading < headings.length) element = headings[item.heading].element; else if (typeof item.fragment === "string" && item.fragment.length <= 2048) element = item.fragment ? document.getElementById(item.fragment) : document.body || document.documentElement; if (!visible(element)) continue; const range = document.createRange(); range.selectNodeContents(element); range.collapse(true); targets.push({ row:item.row, element, range }); } targets.sort((a, b) => a.range.compareBoundaryPoints(Range.START_TO_START, b.range) || a.row - b.row); navigationTargets = targets; navigationRevision = args.revision; return true; } function currentNavigation(position) { // DOM order is stable across zoom/reflow. Avoid measuring every target's // layout at each 400 ms location poll, even for a 20,000-item EPUB nav. let low = 0, high = navigationTargets.length; while (low < high) { const mid = (low + high) >>> 1; if (navigationTargets[mid].range.compareBoundaryPoints(Range.START_TO_START, position) <= 0) low = mid + 1; else high = mid; } for (let i = low - 1; i >= 0; --i) if (visible(navigationTargets[i].element)) return navigationTargets[i].row; return -1; } function positionRange(range, saved) { const node = range.startContainer; const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement; if (!element || !visible(element)) return false; element.scrollIntoView({ block: "start", inline: "start", behavior: "instant" }); const rect = range.getBoundingClientRect(); const a = axis(); const offset = a.vertical ? Number(saved.offsetX) : Number(saved.offsetY); const fraction = Number.isFinite(offset) && Math.abs(offset) <= 2 ? offset : 0; if (a.vertical) scrollBy({ left: rect.left - fraction * innerWidth, behavior: "instant" }); else scrollBy({ top: rect.top - fraction * innerHeight, behavior: "instant" }); restoredRange = { range: range.cloneRange(), x: scrollX, y: scrollY }; return true; } function restore(saved) { if (!saved || typeof saved !== "object") return { restored: false, approximate: true }; let range = resolveCfi(saved.cfi); if (range && positionRange(range, saved)) return { restored: true, approximate: false }; const fragment = typeof saved.fragment === "string" ? saved.fragment.slice(0, 1024) : ""; const element = fragment ? document.getElementById(fragment) : null; if (element && visible(element)) { restoredRange = null; element.scrollIntoView({ block: "start", inline: "start", behavior: "instant" }); // A failed character-level CFI resolved only to its containing ID. return { restored: true, approximate: Boolean(saved.cfi) }; } const quote = typeof saved.textQuote === "string" ? saved.textQuote.slice(0, 160) : ""; if (quote) { const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT); let node, inspected = 0; while ((node = walker.nextNode()) && ++inspected < 100000) { const offset = node.textContent.indexOf(quote); if (offset >= 0 && visible(node.parentElement)) { range = document.createRange(); range.setStart(node, offset); range.collapse(true); if (positionRange(range, saved)) return { restored: true, approximate: true }; } } } setProgression(saved.progression); return { restored: true, approximate: true }; } function heading(direction) { if (!headings.length) headingIndex(); const a = axis(); const candidates = direction > 0 ? headings : headings.slice().reverse(); const seen = new Set(); for (const h of candidates) { const rect = h.element.getBoundingClientRect(); const distance = a.vertical ? (a.reverse ? innerWidth - rect.right : rect.left) : rect.top; const position = Math.round(distance); if (seen.has(position)) continue; seen.add(position); if ((direction > 0 && distance > 3) || (direction < 0 && distance < -3)) { const x = scrollX, y = scrollY; h.element.scrollIntoView({ block: "start", inline: "start", behavior: "instant" }); // Several final headings can clamp to the same destination. // Check the current layout's actual result, so a repeated // destination cannot masquerade as movement or block a boundary. const after = h.element.getBoundingClientRect(); if (Math.abs(scrollX - x) < 1 && Math.abs(scrollY - y) < 1 && Math.abs(after.left - rect.left) < 1 && Math.abs(after.top - rect.top) < 1) continue; return { moved: true, title: h.title }; } } return { moved: false, boundary: direction, empty: !headings.length }; } function applyOptions(next) { options = Object.assign({}, options, next || {}); if (!options.fixed && options.publisherStyle === false) { const size = Number(options.fontSize), height = Number(options.lineHeight); if (!originalTypography) { const body = document.body || document.documentElement; const base = parseFloat(getComputedStyle(body).fontSize) || 16; const nodes = [document.documentElement, body]; // Inline priorities override authored fixed-pixel declarations. // Relative heading/ruby sizes survive; DOM nodes and CFI paths do not change. for (const node of body.querySelectorAll("*")) { if (node.namespaceURI !== "http://www.w3.org/1999/xhtml") continue; if (Array.from(node.childNodes).some(child => child.nodeType === Node.TEXT_NODE && child.textContent.trim())) nodes.push(node); } originalTypography = Array.from(new Set(nodes)).map(node => ({ node, hadStyle: node.hasAttribute("style"), font: node.style.getPropertyValue("font-size"), fontPriority: node.style.getPropertyPriority("font-size"), line: node.style.getPropertyValue("line-height"), linePriority: node.style.getPropertyPriority("line-height"), ratio: node === document.documentElement || node === body ? 1 : (parseFloat(getComputedStyle(node).fontSize) || base) / base })); } for (const entry of originalTypography) { if (size >= 12 && size <= 40) entry.node.style.setProperty("font-size", size * entry.ratio + "px", "important"); if (height >= 1 && height <= 2.5) entry.node.style.setProperty("line-height", String(height), "important"); } } else if (originalTypography) { for (const entry of originalTypography) { if (entry.font) entry.node.style.setProperty("font-size", entry.font, entry.fontPriority); else entry.node.style.removeProperty("font-size"); if (entry.line) entry.node.style.setProperty("line-height", entry.line, entry.linePriority); else entry.node.style.removeProperty("line-height"); if (!entry.hadStyle && !entry.node.getAttribute("style")) entry.node.removeAttribute("style"); } originalTypography = null; } return { fixed: !!options.fixed, fixedViewport: viewport() }; } function restoreLinkOutline() { if (!focusedLink || !linkOutline) return; for (const entry of linkOutline.properties) { if (entry.value) focusedLink.style.setProperty(entry.name, entry.value, entry.priority); else focusedLink.style.removeProperty(entry.name); } if (!linkOutline.hadStyle && !focusedLink.getAttribute("style")) focusedLink.removeAttribute("style"); linkOutline = null; } function clearLinkSelection() { restoreLinkOutline(); focusedLink = null; // selectedLink also supports a natively focused anchor. Clear both // routes so Enter cannot follow a link after selection is cancelled. const active = document.activeElement; if (active && active.localName === "a") active.blur(); return { cleared: true }; } function focusLink(direction) { const links = Array.from(document.querySelectorAll("a[href]")).filter(visible).slice(0, 20000); if (!links.length) return { empty: true }; const current = links.indexOf(focusedLink || document.activeElement); const index = current < 0 ? (direction > 0 ? 0 : links.length - 1) : (current + direction + links.length) % links.length; restoreLinkOutline(); focusedLink = links[index]; linkOutline = { hadStyle: focusedLink.hasAttribute("style"), properties: ["outline-color", "outline-style", "outline-width", "outline-offset"].map(name => ({ name, value: focusedLink.style.getPropertyValue(name), priority: focusedLink.style.getPropertyPriority(name) })) }; focusedLink.style.outline = "2px solid #008577"; focusedLink.style.outlineOffset = "3px"; focusedLink.focus({ preventScroll: false }); return { focused: true, title: text(focusedLink), index, count: links.length }; } function selectedLink() { const element = focusedLink && focusedLink.isConnected ? focusedLink : document.activeElement; if (!element || element.localName !== "a" || !element.hasAttribute("href")) return { empty: true }; try { const url = new URL(element.getAttribute("href"), document.baseURI); if (!["doc:", "http:", "https:"].includes(url.protocol) || url.username || url.password) return { blocked: true }; if (url.protocol === "doc:" && url.host !== location.host) return { blocked: true }; return { url: url.href.slice(0, 8192), title: text(element) }; } catch (_) { return { blocked: true }; } } function executeCommand(id, args) { args = args && typeof args === "object" ? args : {}; const a = axis(); const step = Math.max(1, Math.min(100, Number(args.count) || 1)); switch (id) { case "resource.issues": { const result = resourceIssues; resourceIssues = Object.create(null); return result; } case "index": return index(); case "location": return locate(); case "navigation.configure": return configureNavigation(args); case "location.restore": return restore(args); case "layout": return applyOptions(args); case "link.next": return focusLink(1); case "link.previous": return focusLink(-1); case "link.clear": return clearLinkSelection(); case "link.activate": return selectedLink(); case "scroll.down": scrollBy({ top: 48 * step, behavior: "instant" }); break; case "scroll.up": scrollBy({ top: -48 * step, behavior: "instant" }); break; case "scroll.left": scrollBy({ left: -48 * step, behavior: "instant" }); break; case "scroll.right": scrollBy({ left: 48 * step, behavior: "instant" }); break; case "scroll.half_down": case "scroll.half_up": case "scroll.page_down": case "scroll.page_up": { const forward = id.endsWith("down") ? 1 : -1; const size = id.includes("half") ? 0.5 : 0.9; scrollBy(a.vertical ? { left: innerWidth * size * forward * (a.reverse ? -1 : 1), behavior: "instant" } : { top: innerHeight * size * forward, behavior: "instant" }); break; } case "nav.document_start": setProgression(0); break; case "nav.document_end": setProgression(1); break; case "nav.heading_next": return heading(1); case "nav.heading_previous": return heading(-1); case "heading.first": if (!headings.length) headingIndex(); if (headings.length) headings[0].element.scrollIntoView({block:"start",inline:"start"}); return {moved:!!headings.length}; case "heading.last": if (!headings.length) headingIndex(); if (headings.length) headings[headings.length-1].element.scrollIntoView({block:"start",inline:"start"}); return {moved:!!headings.length}; case "heading.index": { const index = Number(args.index); if (Number.isInteger(index) && index >= 0 && index < headings.length) headings[index].element.scrollIntoView({ block: "start", inline: "start", behavior: "instant" }); break; } default: return { handled: false }; } return { handled: true }; } function command(id, args) { const passive = ["location", "index", "navigation.configure", "resource.issues"].includes(id); if (!passive) { cancelResourceRestore(); userScrollPending = false; } try { return executeCommand(id, args); } finally { if (!passive && document.body) locate(); } } function block(event) { event.preventDefault(); event.stopImmediatePropagation(); } document.addEventListener("submit", block, true); document.addEventListener("reset", block, true); document.addEventListener("beforeinput", block, true); document.addEventListener("drop", block, true); document.addEventListener("click", event => { if (event.target.closest && event.target.closest("button,input,select,textarea,[contenteditable]")) block(event); }, true); document.addEventListener("keydown", event => { if (event.target.closest && event.target.closest("button,input,select,textarea,[contenteditable]") && !(event.ctrlKey && /^(a|c)$/i.test(event.key))) block(event); }, true); document.addEventListener("play", event => { if (event.target.pause) event.target.pause(); }, true); globalThis.__docviewReader = Object.freeze({ command, index, location: locate, restore }); })();