@readium/navigator
Version:
Next generation SDK for publications in Web Apps
120 lines (100 loc) • 4.25 kB
text/typescript
import { Link, Publication } from "@readium/shared";
import { Injector } from "../injection/Injector.ts";
export class WebPubBlobBuilder {
private readonly item: Link;
private readonly burl: string;
private readonly pub: Publication;
private readonly cssProperties?: { [key: string]: string };
private readonly injector: Injector | null = null;
constructor(
pub: Publication,
baseURL: string,
item: Link,
options: {
cssProperties?: { [key: string]: string };
injector?: Injector | null;
}
) {
this.pub = pub;
this.item = item;
this.burl = item.toURL(baseURL) || "";
this.cssProperties = options.cssProperties;
this.injector = options.injector ?? null;
}
public async build(): Promise<string> {
if (!this.item.mediaType.isHTML) {
throw new Error(`Unsupported media type for WebPub: ${this.item.mediaType.string}`);
}
return await this.buildHtmlFrame();
}
private async buildHtmlFrame(): Promise<string> {
// Load the HTML resource
const txt = await this.pub.get(this.item).readAsString();
if(!txt) throw new Error(`Failed reading item ${this.item.href}`);
const doc = new DOMParser().parseFromString(
txt,
this.item.mediaType.string as DOMParserSupportedType
);
const perror = doc.querySelector("parsererror");
if(perror) {
const details = perror.querySelector("div");
throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
}
// Apply resource injections if injection service is provided
if (this.injector) {
await this.injector.injectForDocument(doc, this.item);
}
return this.finalizeDOM(doc, this.burl, this.item.mediaType, txt, this.cssProperties);
}
private setProperties(cssProperties: { [key: string]: string }, doc: Document) {
for (const key in cssProperties) {
const value = cssProperties[key];
if (value) doc.documentElement.style.setProperty(key, value);
}
}
private finalizeDOM(doc: Document, base: string | undefined, mediaType: any, txt?: string, cssProperties?: { [key: string]: string }): string {
if(!doc) return "";
if (cssProperties) {
this.setProperties(cssProperties, doc);
}
doc.body.querySelectorAll("img").forEach((img) => {
img.setAttribute("fetchpriority", "high");
});
if(base !== undefined) {
const b = doc.createElement("base");
b.href = base;
b.dataset.readium = "true";
doc.head.firstChild!.before(b);
}
// Serialize properly based on content type
let serializedContent: string;
if (mediaType.string === "application/xhtml+xml") {
// XHTML: Use XMLSerializer for proper XML formatting
serializedContent = new XMLSerializer().serializeToString(doc);
} else {
// HTML: Use custom HTML serialization to preserve HTML formatting
serializedContent = this.serializeAsHTML(doc, txt || "");
}
// Make blob from doc
return URL.createObjectURL(
new Blob([serializedContent], {
type: mediaType.isHTML
? mediaType.string
: "application/xhtml+xml",
})
);
}
private serializeAsHTML(doc: Document, txt: string): string {
// For HTML content, try to preserve the original HTML structure
// while injecting our scripts
// Extract the original DOCTYPE if present
const doctypeMatch = txt.match(/<!DOCTYPE[^>]*>/i);
const doctype = doctypeMatch ? doctypeMatch[0] + "\n" : "";
// Get the HTML element and serialize it as HTML
const htmlElement = doc.documentElement;
let htmlContent = htmlElement.outerHTML;
// Try to preserve the original HTML structure
// This is a best-effort approach since there's no perfect HTML serializer
return doctype + htmlContent;
}
}