From c5f9dad0f9bb7c0ad7145dbff1f67c431f2af621 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 14 Oct 2025 12:51:11 -0600 Subject: [PATCH] firefox --- firefox/background.html | 10 + firefox/background.js | 28 + firefox/content.js | 259 +++++++++ firefox/firefox.zip | Bin 0 -> 53999 bytes firefox/images/icon128.png | Bin 0 -> 27637 bytes firefox/images/icon16.png | Bin 0 -> 673 bytes firefox/images/icon32.png | Bin 0 -> 2012 bytes firefox/images/icon48.png | Bin 0 -> 4576 bytes firefox/index.css | 106 ++++ firefox/index.html | 45 ++ firefox/index.js | 107 ++++ firefox/manifest.json | 50 ++ firefox/safaribooks/cookies.js | 64 +++ firefox/safaribooks/downloader.js | 850 ++++++++++++++++++++++++++++++ firefox/safaribooks/utils.js | 122 +++++ firefox/safaribooks/zip.js | 151 ++++++ scripts/build-xpi.ps1 | 24 + scripts/build-xpi.sh | 29 + 18 files changed, 1845 insertions(+) create mode 100644 firefox/background.html create mode 100644 firefox/background.js create mode 100644 firefox/content.js create mode 100644 firefox/firefox.zip create mode 100644 firefox/images/icon128.png create mode 100644 firefox/images/icon16.png create mode 100644 firefox/images/icon32.png create mode 100644 firefox/images/icon48.png create mode 100644 firefox/index.css create mode 100644 firefox/index.html create mode 100644 firefox/index.js create mode 100644 firefox/manifest.json create mode 100644 firefox/safaribooks/cookies.js create mode 100644 firefox/safaribooks/downloader.js create mode 100644 firefox/safaribooks/utils.js create mode 100644 firefox/safaribooks/zip.js create mode 100644 scripts/build-xpi.ps1 create mode 100644 scripts/build-xpi.sh diff --git a/firefox/background.html b/firefox/background.html new file mode 100644 index 0000000..20de2b5 --- /dev/null +++ b/firefox/background.html @@ -0,0 +1,10 @@ + + + + + Reilly Downloader Background + + + + + diff --git a/firefox/background.js b/firefox/background.js new file mode 100644 index 0000000..af97fbb --- /dev/null +++ b/firefox/background.js @@ -0,0 +1,28 @@ +import { SafariBooksDownloader } from "./safaribooks/downloader.js"; +import { collectSessionCookies } from "./safaribooks/cookies.js"; + +const runtime = typeof browser !== "undefined" ? browser : chrome; + +const downloader = new SafariBooksDownloader((message) => { + console.log(`[BackgroundDownloader] ${message}`); +}); + +runtime.runtime.onMessage.addListener(async (message) => { + if (message?.type !== "downloadBook") { + return undefined; + } + + const { bookId, theme = "none", kindle = false } = message; + if (!bookId) { + return { ok: false, error: "Missing book ID." }; + } + + try { + await collectSessionCookies(); + await downloader.download(bookId, { theme, kindle }); + return { ok: true }; + } catch (error) { + console.error("[BackgroundDownloader] Download failed:", error); + return { ok: false, error: error.message }; + } +}); diff --git a/firefox/content.js b/firefox/content.js new file mode 100644 index 0000000..edbd50d --- /dev/null +++ b/firefox/content.js @@ -0,0 +1,259 @@ +(function () { + const BUTTON_ID = "reilly-safaribooks-download"; + const BOOK_PATH_PART = "/library/view/"; + let observerInstance = null; + let ensureIntervalId = null; + let initialized = false; + const runtime = typeof browser !== "undefined" ? browser : chrome; + const rawChrome = typeof chrome !== "undefined" ? chrome : null; + + const TITLE_SELECTORS = [ + '[data-test="book-header-title"]', + '[data-testid="book-title"]', + '[data-testid="content-header-title"]', + '[data-testid="content-header"] h1', + '[data-testid="book-detail-header"] h1', + '[data-ui-test="book-title"]', + '[role="heading"][aria-level="1"]', + "main h1", + "article h1", + "section h2", + "article h2", + "header h1", + "h1", + "h2" + ]; + + function extractBookId() { + const segments = window.location.pathname.split("/").filter(Boolean).reverse(); + const match = segments.find((segment) => /^[0-9]{9,}$/.test(segment)); + return match || null; + } + + function isBookPage() { + return window.location.pathname.includes(BOOK_PATH_PART); + } + + function findTitleElement() { + for (const selector of TITLE_SELECTORS) { + const element = document.querySelector(selector); + if (element && element.textContent.trim().length > 0) { + return element; + } + } + const allHeadings = Array.from(document.querySelectorAll("h1")) + .concat(Array.from(document.querySelectorAll('[role="heading"][aria-level="1"]'))); + return allHeadings.find((element) => element.textContent.trim().length > 0) || null; + } + + function ensureFloatingButton(bookId) { + const floatingId = `${BUTTON_ID}-floating`; + let floating = document.getElementById(floatingId); + if (!floating) { + floating = document.createElement("button"); + floating.id = floatingId; + floating.type = "button"; + floating.textContent = "Download EPUB"; + floating.style.position = "fixed"; + floating.style.top = "96px"; + floating.style.right = "24px"; + floating.style.zIndex = "2147483647"; + floating.style.padding = "8px 14px"; + floating.style.borderRadius = "999px"; + floating.style.border = "none"; + floating.style.background = "#3a7bd5"; + floating.style.color = "#fff"; + floating.style.fontSize = "0.95rem"; + floating.style.cursor = "pointer"; + floating.style.boxShadow = "0 10px 25px rgba(58,123,213,0.35)"; + floating.addEventListener("mouseenter", () => { + floating.style.background = "#255ca0"; + }); + floating.addEventListener("mouseleave", () => { + floating.style.background = "#3a7bd5"; + }); + document.body.appendChild(floating); + } + + floating.dataset.bookId = bookId; + floating.onclick = () => handleDownload(floating, bookId); + } + + function createButton() { + const button = document.createElement("button"); + button.id = BUTTON_ID; + button.type = "button"; + button.textContent = "Download EPUB"; + button.style.padding = "6px 12px"; + button.style.borderRadius = "4px"; + button.style.border = "none"; + button.style.background = "#3a7bd5"; + button.style.color = "#fff"; + button.style.fontSize = "0.9rem"; + button.style.cursor = "pointer"; + button.style.display = "inline-flex"; + button.style.alignItems = "center"; + button.style.gap = "4px"; + button.style.boxShadow = "0 1px 2px rgba(0,0,0,0.12)"; + button.style.transition = "background 0.2s ease"; + button.addEventListener("mouseenter", () => { + button.style.background = "#2f6bbd"; + }); + button.addEventListener("mouseleave", () => { + button.style.background = "#3a7bd5"; + }); + return button; + } + + function sendRuntimeMessage(message) { + try { + const result = runtime.runtime.sendMessage(message); + if (result && typeof result.then === "function") { + return result; + } + } catch (error) { + return Promise.reject(error); + } + + return new Promise((resolve, reject) => { + runtime.runtime.sendMessage(message, (response) => { + const err = rawChrome?.runtime?.lastError; + if (err) { + reject(new Error(err.message)); + } else { + resolve(response); + } + }); + }); + } + + function handleDownload(button, bookId) { + if (!bookId) { + return; + } + const originalText = button.textContent; + button.disabled = true; + button.style.opacity = "0.7"; + button.textContent = "Downloading…"; + + sendRuntimeMessage({ type: "downloadBook", bookId }) + .then((response) => { + if (response?.ok) { + button.textContent = "Download started"; + } else { + button.textContent = response?.error ? `Failed: ${response.error}` : "Download failed"; + } + }) + .catch((error) => { + console.error("SafariBooks Downloader:", error); + button.textContent = "Failed (extension error)"; + }) + .finally(() => { + setTimeout(() => { + button.textContent = originalText; + button.disabled = false; + button.style.opacity = ""; + }, 2000); + }); + } + + function injectButton() { + const bookId = extractBookId(); + if (!bookId) { + return; + } + + const titleElement = findTitleElement(); + if (!titleElement) { + ensureFloatingButton(bookId); + return; + } + + const floating = document.getElementById(`${BUTTON_ID}-floating`); + if (floating) { + floating.remove(); + } + + const existing = document.getElementById(BUTTON_ID); + if (existing) { + existing.dataset.bookId = bookId; + return; + } + + const button = createButton(); + button.dataset.bookId = bookId; + button.addEventListener("click", () => handleDownload(button, bookId)); + + const wrapperId = `${BUTTON_ID}-wrapper`; + let wrapper = document.getElementById(wrapperId); + if (!wrapper) { + wrapper = document.createElement("span"); + wrapper.id = wrapperId; + wrapper.style.display = "inline-flex"; + wrapper.style.alignItems = "center"; + wrapper.style.gap = "12px"; + wrapper.style.marginLeft = "12px"; + wrapper.appendChild(button); + + if (titleElement.parentElement) { + titleElement.parentElement.insertBefore(wrapper, titleElement.nextSibling); + } else { + titleElement.insertAdjacentElement("afterend", wrapper); + } + } else if (!wrapper.contains(button)) { + wrapper.appendChild(button); + } + } + + function observeForTitle() { + if (observerInstance) { + return; + } + + observerInstance = new MutationObserver(() => { + if (!isBookPage()) { + return; + } + injectButton(); + }); + + observerInstance.observe(document.body, { childList: true, subtree: true }); + } + + function init() { + if (!isBookPage()) { + return; + } + console.debug("[SafariBooks Downloader] content script active on", window.location.href); + if (initialized) { + injectButton(); + return; + } + + initialized = true; + injectButton(); + observeForTitle(); + window.addEventListener("popstate", injectButton); + const originalPushState = history.pushState; + history.pushState = function (...args) { + originalPushState.apply(this, args); + setTimeout(injectButton, 50); + }; + const originalReplaceState = history.replaceState; + history.replaceState = function (...args) { + originalReplaceState.apply(this, args); + setTimeout(injectButton, 50); + }; + ensureIntervalId = window.setInterval(() => { + if (isBookPage()) { + injectButton(); + } + }, 2000); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init, { once: true }); + } else { + init(); + } +})(); diff --git a/firefox/firefox.zip b/firefox/firefox.zip new file mode 100644 index 0000000000000000000000000000000000000000..28f7772860ff116ba89ee4268af4f21b30902eb2 GIT binary patch literal 53999 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 JYxCy2{{zU0gaiNp literal 0 HcmV?d00001 diff --git a/firefox/images/icon128.png b/firefox/images/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..dd8481220cc94f91373d3fd254096ab585263a48 GIT binary patch literal 27637 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 literal 0 HcmV?d00001 diff --git a/firefox/images/icon16.png b/firefox/images/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..1d7d3ed83cd94ee90051d266d7f94ae41f424bf4 GIT binary patch literal 673 zcmV;S0$%-zP)sLXjGcwXLcm}&F%b=g zF)<`GYD1wV#uD6(Mxa<=DY)azzIY^_+&Skz^UptbW{y`Xm1MtdAGjan_uoBg%Neju zu)VUGZ;v^LetR5HPw;i|2d!O$YB^e}XKPoE*5gBzPs5#qv~=e9vba=F;^8Xz?C^LQ zNO^aN65=;NA^^`ruXQ_jWrEuBoK&IKCQh*iOFRxu%}A!5olG0jm`36W&=_<{^;C#w zdea%{ycooNB$*~kXNgirkfy^(I)j8M#HxvD3}uDs@ocvwqT(o_;)YNIeeeTXMow~c z><8Z=BJg9;rv2J0#QquyZHgDZFZ2HMPmW!F0e&rk+mj&Xv$na- zjVGVzeK<#KYlCd>GqM++lGrSA{=qwjAAjU;aT7F_>Z-Red-pU>U%@3)NJ9!Q*$m+! zT-XGShSQ2u5QNO$J5`mlTS0e6hWzvZ`Kf*iQ+?zoFH(4JXCH;he&x(RKwU00000NkvXX Hu0mjfUj!}x literal 0 HcmV?d00001 diff --git a/firefox/images/icon32.png b/firefox/images/icon32.png new file mode 100644 index 0000000000000000000000000000000000000000..d062b334470a73fb143332b8ca141550f29682c3 GIT binary patch literal 2012 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 + + + + SafariBooks Downloader + + + +

+
+

SafariBooks Downloader

+

Port of safaribooks.py for Chrome. Make sure you are signed in to O'Reilly in this profile.

+
+
+
+ + + +
+ +
+
+
+
+

Activity

+

+      
+
+ + + diff --git a/firefox/index.js b/firefox/index.js new file mode 100644 index 0000000..88bc558 --- /dev/null +++ b/firefox/index.js @@ -0,0 +1,107 @@ +import { SafariBooksDownloader } from "./safaribooks/downloader.js"; +import { collectSessionCookies } from "./safaribooks/cookies.js"; + +const runtime = typeof browser !== "undefined" ? browser : chrome; +const rawChrome = typeof chrome !== "undefined" ? chrome : null; + +const form = document.getElementById("download-form"); +const bookIdInput = document.getElementById("book-id"); +const themeSelect = document.getElementById("theme"); +const kindleCheckbox = document.getElementById("kindle"); +const logView = document.getElementById("log"); +const downloadButton = document.getElementById("start-download"); + +function appendLog(message) { + const timestamp = new Date().toLocaleTimeString(); + logView.textContent += `[${timestamp}] ${message}\n`; + logView.scrollTop = logView.scrollHeight; +} + +const downloader = new SafariBooksDownloader(appendLog); + +function setDisabled(disabled) { + downloadButton.disabled = disabled; + bookIdInput.disabled = disabled; + themeSelect.disabled = disabled; + kindleCheckbox.disabled = disabled; +} + +function extractBookIdFromUrl(url) { + try { + const { pathname } = new URL(url); + const segments = pathname.split("/").filter(Boolean).reverse(); + return segments.find((segment) => /^[0-9]{9,}$/.test(segment)) || null; + } catch (_) { + return null; + } +} + +async function detectActiveBook() { + const tabsApi = runtime.tabs; + if (!tabsApi || typeof tabsApi.query !== "function") { + return; + } + + try { + let activeTab; + if (tabsApi.query.length <= 1) { + const tabs = await tabsApi.query({ active: true, currentWindow: true }); + activeTab = Array.isArray(tabs) ? tabs[0] : null; + } else { + activeTab = await new Promise((resolve, reject) => { + tabsApi.query({ active: true, currentWindow: true }, (tabs) => { + const err = rawChrome?.runtime?.lastError; + if (err) { + reject(new Error(err.message)); + } else { + resolve(Array.isArray(tabs) ? tabs[0] : null); + } + }); + }); + } + + if (!activeTab?.url || !activeTab.url.includes("/library/view/")) { + return; + } + + const detectedBookId = extractBookIdFromUrl(activeTab.url); + if (!detectedBookId) { + return; + } + + if (bookIdInput.value !== detectedBookId) { + bookIdInput.value = detectedBookId; + appendLog(`Detected book ID ${detectedBookId} from the active tab.`); + } + } catch (error) { + console.warn("SafariBooks Downloader: unable to detect active book.", error); + } +} + +form.addEventListener("submit", async (event) => { + event.preventDefault(); + const bookId = bookIdInput.value.trim(); + if (!bookId) { + appendLog("Please enter a book ID."); + return; + } + setDisabled(true); + appendLog("Verifying O'Reilly session..."); + try { + const session = await collectSessionCookies(); + appendLog(`Session OK (cookies detected: ${session.count}).`); + appendLog(`Starting EPUB download for ${bookId}...`); + await downloader.download(bookId, { + theme: themeSelect.value, + kindle: kindleCheckbox.checked + }); + appendLog("Download completed."); + } catch (error) { + console.error(error); + appendLog(`Download failed: ${error.message}`); + } finally { + setDisabled(false); + } +}); + +detectActiveBook(); diff --git a/firefox/manifest.json b/firefox/manifest.json new file mode 100644 index 0000000..9940134 --- /dev/null +++ b/firefox/manifest.json @@ -0,0 +1,50 @@ +{ + "manifest_version": 2, + "name": "Reilly SafariBooks Downloader", + "description": "Download O'Reilly learning content as EPUB files using a port of safaribooks.py.", + "version": "0.1.0", + "browser_specific_settings": { + "gecko": { + "id": "reilly@safaribooks", + "strict_min_version": "115.0" + } + }, + "icons": { + "16": "images/icon16.png", + "32": "images/icon32.png", + "48": "images/icon48.png", + "128": "images/icon128.png" + }, + "background": { + "page": "background.html" + }, + "browser_action": { + "default_title": "SafariBooks Downloader", + "default_popup": "index.html", + "default_icon": { + "16": "images/icon16.png", + "32": "images/icon32.png", + "48": "images/icon48.png" + } + }, + "content_scripts": [ + { + "matches": [ + "https://learning.oreilly.com/library/view/*" + ], + "js": [ + "content.js" + ], + "run_at": "document_idle" + } + ], + "permissions": [ + "downloads", + "cookies", + "tabs", + "storage", + "https://learning.oreilly.com/*", + "https://www.oreilly.com/*", + "https://api.oreilly.com/*" + ] +} diff --git a/firefox/safaribooks/cookies.js b/firefox/safaribooks/cookies.js new file mode 100644 index 0000000..d5ac3a3 --- /dev/null +++ b/firefox/safaribooks/cookies.js @@ -0,0 +1,64 @@ +const runtime = typeof browser !== "undefined" ? browser : chrome; +const rawChrome = typeof chrome !== "undefined" ? chrome : null; + +const PROFILE_URL = "https://learning.oreilly.com/profile/"; +const COOKIE_DOMAINS = ["learning.oreilly.com", ".oreilly.com", "www.oreilly.com"]; + +function callCookiesGetAll(filter) { + const result = runtime.cookies.getAll(filter); + if (result && typeof result.then === "function") { + return result; + } + + return new Promise((resolve, reject) => { + runtime.cookies.getAll(filter, (cookies) => { + const err = rawChrome?.runtime?.lastError; + if (err) { + reject(new Error(err.message)); + } else { + resolve(cookies); + } + }); + }); +} + +async function gatherCookies() { + const collected = {}; + for (const domain of COOKIE_DOMAINS) { + const cookies = await callCookiesGetAll({ domain }); + cookies.forEach((cookie) => { + collected[cookie.name] = cookie.value; + }); + } + return collected; +} + +export async function collectSessionCookies() { + let response; + try { + response = await fetch(PROFILE_URL, { credentials: "include" }); + } catch (error) { + throw new Error(`Unable to reach O'Reilly profile page: ${error.message}`); + } + + if (response.status === 401 || response.status === 403) { + throw new Error("You must be logged into learning.oreilly.com in this profile before downloading."); + } + + if (!response.ok) { + throw new Error(`Profile check failed (status ${response.status}).`); + } + + const jar = await gatherCookies(); + const count = Object.keys(jar).length; + if (!count) { + throw new Error("No O'Reilly cookies detected. Make sure you visited learning.oreilly.com in this profile."); + } + + return { + status: response.status, + count, + cookies: jar + }; +} + diff --git a/firefox/safaribooks/downloader.js b/firefox/safaribooks/downloader.js new file mode 100644 index 0000000..5516faa --- /dev/null +++ b/firefox/safaribooks/downloader.js @@ -0,0 +1,850 @@ +import { + makeFolderFriendlyName, + makeFileFriendlyName, + escapeXml, + isAbsoluteUrl, + isDocLink, + isImageLink, + makeValidId, + resolveStylesRelativePath, + ensureForwardSlashes, + uniqPush, + dataFromString, + isFontUrl, + isLikelyImageAsset +} from "./utils.js"; +import { SimpleZip } from "./zip.js"; + +const SAFARI_BASE_URL = "https://learning.oreilly.com"; +const API_V2_TEMPLATE = `${SAFARI_BASE_URL}/api/v2/epubs/urn:orm:book:`; +const API_V1_TEMPLATE = `${SAFARI_BASE_URL}/api/v1/book/`; +const PROFILE_URL = `${SAFARI_BASE_URL}/profile/`; +const IMAGES_BASE = "Images/"; + +const THEME_CSS = { + white: ".ucvMode-white{background:#ffffff;color:#101010;}", + sepia: ".ucvMode-sepia{background:#f4ecd8;color:#523f2a;} .ucvMode-sepia a{color:#274060;}", + black: ".ucvMode-black{background:#070707;color:#f4f4f5;} .ucvMode-black a{color:#60a5fa;}" +}; + +const KINDLE_CSS = + "#sbo-rt-content *{word-wrap:break-word!important;word-break:break-word!important;}" + + "#sbo-rt-content table,#sbo-rt-content pre{overflow-x:unset!important;overflow:unset!important;" + + "overflow-y:unset!important;white-space:pre-wrap!important;}"; + +function downloadWithWebExtensionsApi(options) { + if (typeof browser !== "undefined" && browser?.downloads?.download) { + return browser.downloads.download(options); + } + if (typeof chrome !== "undefined" && chrome?.downloads?.download) { + return new Promise((resolve, reject) => { + chrome.downloads.download(options, (downloadId) => { + const err = chrome.runtime?.lastError; + if (err) { + reject(new Error(err.message)); + } else { + resolve(downloadId); + } + }); + }); + } + return Promise.reject(new Error("Downloads API is not available in this browser.")); +} + +function formatTwoDigits(value) { + return value.toString().padStart(2, "0"); +} + +function buildXhtml(pageCss, innerHtml, themeMode, includeKindleCss, extraThemeCss = "") { + const kindleRule = includeKindleCss ? KINDLE_CSS : ""; + const themeRule = THEME_CSS[themeMode] || ""; + const additional = [themeRule, extraThemeCss].filter(Boolean).join("\n"); + return ( + ` + + + +${pageCss} + + +
${innerHtml}
+` + ); +} + +function isProbablySvgImage(element) { + return element.tagName?.toLowerCase() === "svg"; +} + +function toAbsoluteUrl(link, base) { + try { + return new URL(link, base).toString(); + } catch (_) { + return link; + } +} + +export class SafariBooksDownloader { + constructor(log = console.log) { + this.log = (message) => { + const timestamp = new Date().toISOString(); + log(`[${timestamp}] ${message}`); + }; + this.reset(); + } + + reset() { + this.bookInfo = null; + this.bookId = null; + this.bookChapters = []; + this.chapterDocuments = []; + this.cleanTitle = ""; + this.primaryAuthors = ""; + this.outputFileName = "download"; + this.baseUrl = ""; + this.coverPath = ""; + this.themeMode = "white"; + this.options = { theme: "none", kindle: false }; + + this.cssSources = []; + this.cssFilenameMap = new Map(); + this.cssContentMap = new Map(); + + this.fontDownloads = new Map(); // local path -> absolute url + this.imageDownloads = new Map(); // relative path -> absolute url + this.cssImageDownloads = new Map(); // path within Styles -> absolute url + this.imagesFromLinks = new Set(); + } + + async download(bookId, options = {}) { + this.reset(); + this.bookId = bookId; + this.options = { + theme: options.theme ?? "none", + kindle: Boolean(options.kindle) + }; + this.themeMode = this.options.theme === "none" ? "white" : this.options.theme; + + this.log(`Verifying session by loading profile page...`); + await this.checkLogin(); + + this.log(`Retrieving book metadata for ${bookId}...`); + this.bookInfo = await this.fetchBookInfo(bookId); + this.baseUrl = this.bookInfo.web_url || this.bookInfo.url; + this.cleanTitle = makeFolderFriendlyName(this.bookInfo.title, this.bookId); + const authorNames = (this.bookInfo.authors || []).map((author) => author.name).filter(Boolean); + this.primaryAuthors = authorNames.join(", ") || "Unknown Author"; + this.outputFileName = makeFileFriendlyName(this.bookInfo.title, this.primaryAuthors, this.bookId); + + this.log(`Fetching table of chapters...`); + this.bookChapters = await this.fetchChapters(this.bookInfo.chapters); + this.log(`Chapters discovered: ${this.bookChapters.length}`); + + await this.processChapters(); + + await this.fetchCssSources(); + await this.fetchFonts(); + await this.fetchImages(); + await this.fetchCssImages(); + + const zip = await this.assembleEpub(); + + const blob = new Blob([zip.generate()], { type: "application/epub+zip" }); + const url = URL.createObjectURL(blob); + + const filename = `${this.cleanTitle}/${this.outputFileName}.epub`; + await downloadWithWebExtensionsApi({ url, filename, saveAs: false }); + this.log(`EPUB saved as ${filename}`); + } + + async checkLogin() { + const response = await fetch(PROFILE_URL, { credentials: "include" }); + if (!response.ok) { + throw new Error(`Authentication failed (status ${response.status}) when accessing ${PROFILE_URL}`); + } + const text = await response.text(); + if (text.includes('user_type":"Expired"')) { + throw new Error("Authentication issue: account subscription expired."); + } + } + + async fetchBookInfo(bookId) { + const infoResponse = await this.fetchJson(`${API_V2_TEMPLATE}${bookId}/`); + const v1Response = await this.fetchJson(`${API_V1_TEMPLATE}${bookId}/`); + const merged = { ...infoResponse }; + + for (const key of ["authors", "subjects", "topics", "rights", "publishers", "web_url"]) { + if (v1Response[key]) { + merged[key] = v1Response[key]; + } + } + if (!merged.url && merged.web_url) { + merged.url = merged.web_url; + } + return merged; + } + + async fetchChapters(initialUrl) { + if (!initialUrl) { + throw new Error("Missing chapter list URL in book metadata."); + } + const chapters = []; + let next = initialUrl; + while (next) { + const data = await this.fetchJson(next); + if (!Array.isArray(data.results)) { + throw new Error("Chapter response missing results array."); + } + chapters.push(...data.results); + next = data.next; + } + return chapters; + } + + async fetchJson(url) { + const response = await fetch(url, { credentials: "include" }); + if (!response.ok) { + throw new Error(`Request to ${url} failed with status ${response.status}`); + } + return response.json(); + } + + getChapterFilename(chapter) { + if (chapter.ourn) { + const lastPart = chapter.ourn.split("chapter:").pop(); + return lastPart.split("%2f").pop(); + } + return chapter.filename || `${chapter.title || "chapter"}.xhtml`; + } + + registerCssSource(url) { + const key = ensureForwardSlashes(url); + if (!this.cssFilenameMap.has(key)) { + const index = uniqPush(this.cssSources, key); + const filename = `Style${formatTwoDigits(index)}.css`; + this.cssFilenameMap.set(key, filename); + } + return this.cssFilenameMap.get(key); + } + + registerFont(baseUrl, relativePath) { + const localPath = resolveStylesRelativePath(relativePath); + if (!this.fontDownloads.has(localPath)) { + const absolute = toAbsoluteUrl(relativePath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`); + this.fontDownloads.set(localPath, absolute); + } + } + + registerImage(fullUrl) { + if (!fullUrl) { + return null; + } + const absoluteUrl = isAbsoluteUrl(fullUrl) + ? fullUrl + : toAbsoluteUrl(fullUrl, `${this.bookInfo.url.replace(/\/$/, "")}/files/`); + const local = this.localImagePath(absoluteUrl); + if (!local.fileName) { + return null; + } + const relative = local.folder ? `${local.folder}/${local.fileName}` : local.fileName; + if (!this.imageDownloads.has(relative)) { + this.imageDownloads.set(relative, absoluteUrl); + } + return `${IMAGES_BASE}${relative}`; + } + + localImagePath(url) { + const normalized = url.split("?")[0]; + const imageDirs = ["images/", "graphics/", "assets/"]; + if (!this.bookInfo?.url) { + const fileName = normalized.split("/").pop(); + return { fileName, folder: "" }; + } + const bookUrl = this.bookInfo.url.replace(/\/$/, ""); + const components = bookUrl.split("/"); + const bookIdSegment = components.pop(); + const base = `${bookIdSegment}/files/`; + const relCandidate = normalized.includes(base) + ? normalized.split(base).pop() + : normalized.split("/").slice(-1)[0]; + let trimmed = relCandidate.replace(/^\/+/, ""); + for (const dir of imageDirs) { + if (trimmed.startsWith(dir)) { + trimmed = trimmed.slice(dir.length); + break; + } + } + const parts = trimmed.split("/"); + const fileName = parts.pop(); + return { + fileName, + folder: parts.join("/") + }; + } + + rewriteLink(link) { + if (!link || link.startsWith("mailto")) { + return link; + } + if (!isAbsoluteUrl(link)) { + if (!isDocLink(link) && isImageLink(link)) { + const base = `${this.bookInfo.url.replace(/\/$/, "")}/files/`; + const absolute = toAbsoluteUrl(link, base); + const updated = this.registerImage(absolute); + if (updated) { + this.imagesFromLinks.add(absolute); + return updated; + } + } + return link.split("/").pop().replace(".html", ".xhtml"); + } + if (typeof this.bookInfo.url === "string" && link.includes(this.bookInfo.url)) { + const partial = link.replace(this.bookInfo.url, "").replace(/^\/+/, ""); + return this.rewriteLink(partial); + } + return link; + } + + async processChapters() { + for (let index = 0; index < this.bookChapters.length; index++) { + const chapter = this.bookChapters[index]; + const filename = this.getChapterFilename(chapter).replace(".html", ".xhtml"); + const assets = chapter.related_assets || {}; + + if (Array.isArray(assets.images)) { + assets.images.forEach((img) => { + const url = isAbsoluteUrl(img) ? img : `${this.baseUrl}/files/${img}`; + this.registerImage(url); + }); + } + + const chapterStyles = []; + if (Array.isArray(assets.stylesheets)) { + chapterStyles.push( + ...assets.stylesheets.map((cssUrl) => (isAbsoluteUrl(cssUrl) ? cssUrl : `${this.baseUrl}${cssUrl}`)) + ); + } + if (Array.isArray(assets.site_styles)) { + chapterStyles.push( + ...assets.site_styles.map((cssUrl) => (isAbsoluteUrl(cssUrl) ? cssUrl : `${this.baseUrl}${cssUrl}`)) + ); + } + + const htmlRoot = await this.loadChapterDocument(chapter.content_url); + const firstPage = index === 0; + const { cssMarkup, xhtml, detectedCover } = this.parseHtml(htmlRoot, chapterStyles, firstPage); + if (detectedCover && !this.coverPath) { + const coverRegistered = this.registerImage(detectedCover); + if (coverRegistered) { + this.coverPath = coverRegistered; + } + } + this.chapterDocuments.push({ + title: chapter.title || `Chapter ${index + 1}`, + filename, + cssMarkup, + xhtml + }); + } + + if (!this.coverPath && typeof this.bookInfo.cover === "string") { + this.log("Falling back to book metadata cover image."); + const registered = this.registerImage(this.bookInfo.cover); + if (registered) { + this.coverPath = registered; + } + } + + const hasCoverChapter = this.chapterDocuments.some( + (chapter) => + chapter.filename.toLowerCase().includes("cover") || chapter.title.toLowerCase().includes("cover") + ); + if (!hasCoverChapter && this.coverPath) { + const coverMarkup = `
Book cover
`; + const coverDocument = buildXhtml("", coverMarkup, this.themeMode, this.options.kindle); + this.chapterDocuments.unshift({ + title: "Cover", + filename: "default_cover.xhtml", + cssMarkup: "", + xhtml: coverDocument + }); + } + } + + async loadChapterDocument(url) { + const response = await fetch(url, { credentials: "include" }); + if (!response.ok) { + throw new Error(`Failed to download chapter at ${url} (status ${response.status})`); + } + const htmlText = await response.text(); + const parser = new DOMParser(); + return parser.parseFromString(htmlText, "text/html"); + } + + parseHtml(rootDocument, chapterStylesheets, firstPage) { + const bookContent = rootDocument.querySelector("#sbo-rt-content"); + if (!bookContent) { + throw new Error("Chapter markup missing #sbo-rt-content node."); + } + + const pageCssBlocks = []; + + for (const cssUrl of chapterStylesheets) { + const filename = this.registerCssSource(cssUrl); + pageCssBlocks.push(``); + } + + const stylesheetLinks = rootDocument.querySelectorAll("link[rel='stylesheet']"); + stylesheetLinks.forEach((link) => { + const href = link.getAttribute("href"); + if (!href) { + return; + } + const cssUrl = href.startsWith("//") ? `https:${href}` : toAbsoluteUrl(href, this.baseUrl); + const filename = this.registerCssSource(cssUrl); + pageCssBlocks.push(``); + }); + + const inlineStyles = rootDocument.querySelectorAll("style"); + inlineStyles.forEach((styleEl) => { + if (styleEl.hasAttribute("data-template")) { + styleEl.textContent = styleEl.getAttribute("data-template"); + styleEl.removeAttribute("data-template"); + } + pageCssBlocks.push(styleEl.outerHTML); + }); + + this.rewriteLinksInNode(bookContent); + this.fixOverconstrainedImages(bookContent); + + let detectedCover = null; + if (firstPage) { + detectedCover = this.detectCoverImage(bookContent); + } + + const serializer = new XMLSerializer(); + const xhtml = serializer.serializeToString(bookContent); + const documentHtml = buildXhtml( + pageCssBlocks.join("\n"), + xhtml, + this.themeMode, + this.options.kindle, + this.options.theme !== "none" ? THEME_CSS[this.options.theme] : "" + ); + + return { + cssMarkup: pageCssBlocks.join("\n"), + xhtml: documentHtml, + detectedCover + }; + } + + rewriteLinksInNode(node) { + const elements = node.querySelectorAll("*"); + elements.forEach((el) => { + for (const attr of Array.from(el.attributes)) { + if (attr.name === "href" || attr.name === "src" || attr.name.endsWith(":href")) { + const rewritten = this.rewriteLink(attr.value); + if (rewritten) { + el.setAttribute(attr.name, rewritten); + } + } + } + }); + } + + fixOverconstrainedImages(node) { + const images = node.querySelectorAll("img"); + images.forEach((img) => { + const style = img.getAttribute("style"); + if (style && (style.includes("width") || style.includes("height"))) { + img.removeAttribute("width"); + img.removeAttribute("height"); + } + }); + const svgImages = node.querySelectorAll("svg image"); + svgImages.forEach((imageEl) => { + const hrefAttr = Array.from(imageEl.attributes).find((attr) => attr.name.endsWith("href")); + if (hrefAttr) { + const img = node.ownerDocument.createElement("img"); + img.setAttribute("src", this.rewriteLink(hrefAttr.value)); + const parent = imageEl.parentElement?.parentElement; + if (parent) { + parent.appendChild(img); + } + } + }); + } + + detectCoverImage(node) { + const selector = + "img[id*='cover' i], img[class*='cover' i], img[name*='cover' i], img[src*='cover' i], img[alt*='cover' i]"; + const coverImg = node.querySelector(selector); + if (coverImg) { + return coverImg.getAttribute("src"); + } + const fallback = node.querySelector("img"); + return fallback ? fallback.getAttribute("src") : null; + } + + async fetchCssSources() { + for (const source of this.cssSources) { + if (this.cssContentMap.has(source)) { + continue; + } + const response = await fetch(source, { credentials: "include" }); + if (!response.ok) { + this.log(`Warning: unable to retrieve CSS from ${source} (status ${response.status})`); + continue; + } + let cssContent = await response.text(); + cssContent = cssContent.replace(/display\s*:\s*none/gi, "visibility: hidden"); + this.cssContentMap.set(source, cssContent); + + this.registerFontsAndCssImages(source, cssContent); + } + } + + registerFontsAndCssImages(sourceUrl, cssContent) { + try { + const url = new URL(sourceUrl); + const base = `${url.origin}${url.pathname.substring(0, url.pathname.lastIndexOf("/") + 1)}`; + const urlRegex = /url\(([^)]+)\)/gi; + let match = urlRegex.exec(cssContent); + while (match) { + let raw = match[1].trim().replace(/^['"]|['"]$/g, ""); + if (raw.startsWith("data:") || raw.startsWith("about:")) { + match = urlRegex.exec(cssContent); + continue; + } + const absolute = toAbsoluteUrl(raw, base); + if (isFontUrl(raw)) { + this.registerFont(base, raw); + } else if (isLikelyImageAsset(raw)) { + const localPath = resolveStylesRelativePath(raw); + if (!this.cssImageDownloads.has(localPath)) { + this.cssImageDownloads.set(localPath, absolute); + } + } + match = urlRegex.exec(cssContent); + } + } catch (_) { + // ignore malformed css urls + } + } + + async fetchFonts() { + for (const [localPath, url] of this.fontDownloads.entries()) { + if (this.cssImageDownloads.has(localPath)) { + continue; + } + const response = await fetch(url, { credentials: "include" }); + if (!response.ok) { + this.log(`Warning: unable to retrieve font ${url} (status ${response.status})`); + continue; + } + const data = new Uint8Array(await response.arrayBuffer()); + this.cssImageDownloads.set(localPath, { data, inline: false, sourceUrl: url }); + } + } + + async fetchImages() { + for (const [relative, url] of this.imageDownloads.entries()) { + const result = await this.fetchImageWithFallback(relative, url); + if (result) { + this.imageDownloads.set(relative, result); + } + } + } + + async fetchImageWithFallback(relative, primaryUrl) { + const attempts = []; + const tried = new Set(); + + const queue = [primaryUrl, ...this.buildImageFallbackUrls(relative, primaryUrl)]; + for (const candidate of queue) { + if (!candidate || tried.has(candidate)) { + continue; + } + tried.add(candidate); + try { + const response = await fetch(candidate, { credentials: "include" }); + if (!response.ok) { + attempts.push(`${candidate} (status ${response.status})`); + continue; + } + const data = new Uint8Array(await response.arrayBuffer()); + if (candidate !== primaryUrl) { + this.log(`Info: fetched image ${relative} via fallback ${candidate}`); + } + return { data, sourceUrl: candidate }; + } catch (error) { + attempts.push(`${candidate} (${error.message})`); + } + } + + if (attempts.length) { + this.log( + `Warning: unable to fetch image ${primaryUrl}. Attempts: ${attempts.join("; ")}` + ); + } + return null; + } + + buildImageFallbackUrls(relative, url) { + const candidates = new Set(); + + if (url.includes("/Images/")) { + candidates.add(url.replace("/Images/", "/images/")); + } + if (url.includes("/images/")) { + candidates.add(url.replace("/images/", "/Images/")); + } + + if (url.includes("/files/Images/")) { + const base = url.replace(/Images\/[^/]*$/i, "Images/"); + ["cover.jpg", "cover.jpeg", "cover.png", "cover-large.jpg"].forEach((file) => { + candidates.add(`${base}${file}`); + }); + } + + const isbnMatch = url.match(/Images\/(\d{10,13})\.(jpg|jpeg|png|gif)$/i); + if (isbnMatch) { + const isbn = isbnMatch[1]; + const sizes = ["600w", "400w", "250w", ""]; + sizes.forEach((size) => { + const trimmedSize = size ? `/${size}` : ""; + candidates.add(`https://learning.oreilly.com/library/cover/${isbn}${trimmedSize}/`); + }); + } + + if (this.bookInfo?.cover && url !== this.bookInfo.cover) { + candidates.add(this.bookInfo.cover); + } + + if (relative && this.bookInfo?.cover_url) { + candidates.add(this.bookInfo.cover_url); + } + + return Array.from(candidates).filter(Boolean); + } + + async fetchCssImages() { + for (const [path, entry] of this.cssImageDownloads.entries()) { + if (entry?.data) { + continue; + } + const response = await fetch(entry, { credentials: "include" }); + if (!response.ok) { + this.log(`Warning: unable to fetch CSS image ${entry} (status ${response.status})`); + continue; + } + const data = new Uint8Array(await response.arrayBuffer()); + this.cssImageDownloads.set(path, { data, sourceUrl: entry }); + } + } + + createManifestAndSpine() { + const manifestEntries = []; + const spineEntries = []; + this.chapterDocuments.forEach((chapter) => { + const itemId = escapeXml(chapter.filename.replace(/\.xhtml$/i, "")); + manifestEntries.push( + `` + ); + spineEntries.push(``); + }); + + for (const [relative, image] of this.imageDownloads.entries()) { + if (!image?.data) { + continue; + } + const ext = relative.split(".").pop().toLowerCase(); + const media = ext.includes("jp") ? "image/jpeg" : `image/${ext}`; + const itemId = escapeXml(`img_${relative.replace(/[\\/]/g, "_").split(".")[0]}`); + manifestEntries.push( + `` + ); + } + + for (const [source, filename] of this.cssFilenameMap.entries()) { + const cssContent = this.cssContentMap.get(source); + if (!cssContent) { + continue; + } + manifestEntries.push( + `` + ); + } + + for (const [path] of this.cssImageDownloads.entries()) { + const ext = path.split(".").pop().toLowerCase(); + if (isFontUrl(path)) { + const mediaType = ext === "otf" ? "font/otf" : ext === "ttf" ? "font/ttf" : `font/${ext}`; + const id = escapeXml(path.replace(/[\\/]/g, "_")); + manifestEntries.push( + `` + ); + } else if (isLikelyImageAsset(path)) { + const media = ext.includes("jp") ? "image/jpeg" : `image/${ext}`; + const id = escapeXml(path.replace(/[\\/]/g, "_")); + manifestEntries.push(``); + } + } + + return { + manifest: manifestEntries.join("\n"), + spine: spineEntries.join("\n") + }; + } + + getCoverId() { + if (!this.coverPath) { + return "cover"; + } + const withoutPrefix = this.coverPath.replace(/^Images\//, ""); + const base = withoutPrefix.includes(".") ? withoutPrefix.split(".")[0] : withoutPrefix; + return `img_${base.replace(/[\\/]/g, "_")}`; + } + + createContentOpf(manifest, spine) { + const authors = (this.bookInfo.authors || []) + .map((author) => `${escapeXml(author.name)}`) + .join("\n"); + const subjects = (this.bookInfo.subjects || []) + .map((subject) => `${escapeXml(subject.name)}`) + .join("\n"); + + const descriptionContainer = this.bookInfo.descriptions || {}; + const description = typeof descriptionContainer === "object" ? descriptionContainer["text/plain"] || "" : ""; + const publishers = (this.bookInfo.publishers || []).map((publisher) => escapeXml(publisher.name)).join(", "); + + return ` + + +${escapeXml(this.bookInfo.title)} +${authors} +${escapeXml(description)} +${subjects} +${publishers} +${escapeXml(this.bookInfo.rights || "")} +en-US +${escapeXml(this.bookInfo.publication_date || "")} +${escapeXml(this.bookInfo.isbn || this.bookId)} + + + + +${manifest} + + +${spine} + + +`; + } + + createTocNavMap(entries, count = { value: 0 }, maxDepth = { value: 0 }) { + if (!Array.isArray(entries)) { + return { navMap: "", maxDepth: maxDepth.value }; + } + let navMap = ""; + for (const entry of entries) { + count.value += 1; + const depth = Number(entry.depth || 0); + if (depth > maxDepth.value) { + maxDepth.value = depth; + } + const href = this.getChapterHref(entry); + const title = escapeXml(entry.title || entry.label || "Chapter"); + const referenceId = entry.reference_id ? escapeXml(makeValidId(entry.reference_id)) : ""; + const fragment = entry.fragment ? entry.fragment : referenceId; + let childrenXml = ""; + if (Array.isArray(entry.children) && entry.children.length) { + const childResult = this.createTocNavMap(entry.children, count, maxDepth); + childrenXml = childResult.navMap; + maxDepth.value = Math.max(maxDepth.value, childResult.maxDepth); + } + navMap += `${title}${childrenXml}`; + } + return { navMap, maxDepth: maxDepth.value }; + } + + getChapterHref(entry) { + if (entry.href) { + return escapeXml(entry.href.replace(".html", ".xhtml")); + } + if (entry.ourn) { + const filename = entry.ourn.split("chapter:").pop().split("%2f").pop(); + return escapeXml(filename.replace(".html", ".xhtml")); + } + return escapeXml(this.chapterDocuments[0]?.filename || "chapter.xhtml"); + } + + createToc() { + const sourceToc = this.bookInfo.table_of_contents || []; + const { navMap, maxDepth } = this.createTocNavMap(sourceToc); + const authors = (this.bookInfo.authors || []).map((author) => author.name).join(", "); + return ` + + + + + + + + +${escapeXml(this.bookInfo.title)} +${escapeXml(authors)} +${navMap} +`; + } + + async assembleEpub() { + const zip = new SimpleZip(); + zip.addFile("mimetype", dataFromString("application/epub+zip")); + zip.addFile("META-INF/container.xml", dataFromString('')); + + const { manifest, spine } = this.createManifestAndSpine(); + const contentOpf = this.createContentOpf(manifest, spine); + zip.addFile("OEBPS/content.opf", dataFromString(contentOpf)); + const toc = this.createToc(); + zip.addFile("OEBPS/toc.ncx", dataFromString(toc)); + + for (const chapter of this.chapterDocuments) { + zip.addFile(`OEBPS/${chapter.filename}`, dataFromString(chapter.xhtml)); + } + + for (const [source, filename] of this.cssFilenameMap.entries()) { + const content = this.cssContentMap.get(source); + if (!content) { + continue; + } + zip.addFile(`OEBPS/Styles/${filename}`, dataFromString(content)); + } + + for (const [path, entry] of this.cssImageDownloads.entries()) { + if (!entry?.data) { + continue; + } + zip.addFile(`OEBPS/${path}`, entry.data); + } + + for (const [relative, entry] of this.imageDownloads.entries()) { + if (!entry?.data) { + continue; + } + zip.addFile(`OEBPS/Images/${relative}`, entry.data); + } + + return zip; + } +} diff --git a/firefox/safaribooks/utils.js b/firefox/safaribooks/utils.js new file mode 100644 index 0000000..e338d47 --- /dev/null +++ b/firefox/safaribooks/utils.js @@ -0,0 +1,122 @@ +const INVALID_DIR_CHARS = /[~#%&*{}\\<>?/`'"|+:]/g; +const IMAGE_EXTENSIONS = new Set(["jpg", "jpeg", "png", "gif", "svg", "webp"]); +const FONT_EXTENSIONS = new Set(["otf", "ttf", "woff", "woff2", "eot"]); + +export function cleanDirectoryName(name) { + let sanitized = name; + if (sanitized.includes(":")) { + if (sanitized.indexOf(":") > 15) { + sanitized = sanitized.split(":")[0]; + } else { + sanitized = sanitized.replace(/:/g, ","); + } + } + sanitized = sanitized.replace(INVALID_DIR_CHARS, "_"); + return sanitized.replace(/\s+/g, " ").trim(); +} + +export function makeFolderFriendlyName(title, bookId) { + const safeTitle = cleanDirectoryName(title); + const prefix = safeTitle.split(",").slice(0, 2).join(","); + return `${prefix || safeTitle} (${bookId})`.trim(); +} + +export function makeFileFriendlyName(...segments) { + const cleaned = segments + .map((segment) => segment ?? "") + .map((segment) => cleanDirectoryName(String(segment))) + .filter((segment) => segment.length > 0); + + if (!cleaned.length) { + return "download"; + } + + return cleaned.join(" - ").replace(/\s+/g, " ").trim(); +} + +export function escapeXml(value) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +export function isAbsoluteUrl(url) { + try { + const parsed = new URL(url); + return Boolean(parsed.protocol); + } catch (_) { + return false; + } +} + +export function isDocLink(url) { + return [".html", ".xhtml", ".pdf"].some((ext) => url.includes(ext)); +} + +export function isImageLink(url) { + const lowered = url.toLowerCase(); + if (lowered.includes("cover") || lowered.includes("images") || lowered.includes("graphics")) { + return true; + } + const ext = lowered.split(".").pop(); + return IMAGE_EXTENSIONS.has(ext); +} + +export function isFontUrl(url) { + const ext = url.toLowerCase().split(".").pop().split("?")[0]; + return FONT_EXTENSIONS.has(ext); +} + +export function isLikelyImageAsset(url) { + const ext = url.toLowerCase().split(".").pop().split("?")[0]; + return IMAGE_EXTENSIONS.has(ext); +} + +export function makeValidId(value) { + const sanitized = value.replace(/\W|^(?=\d)/g, "_"); + return sanitized[0]?.match(/\d/) ? `_${sanitized}` : sanitized; +} + +export function resolveStylesRelativePath(relativePath) { + const baseParts = ["Styles"]; + const segments = relativePath.split("/").filter(Boolean); + const resolved = []; + for (const segment of segments) { + if (segment === ".") { + continue; + } + if (segment === "..") { + if (resolved.length) { + resolved.pop(); + } else if (baseParts.length) { + baseParts.pop(); + } + continue; + } + resolved.push(segment); + } + return [...baseParts, ...resolved].join("/"); +} + +export function ensureForwardSlashes(path) { + return path.replace(/\\/g, "/"); +} + +export function uniqPush(collection, value) { + if (!collection.includes(value)) { + collection.push(value); + } + return collection.indexOf(value); +} + +export function dataFromString(value) { + const encoder = new TextEncoder(); + return encoder.encode(value); +} + +export function toHex(value, width = 8) { + return value.toString(16).padStart(width, "0"); +} diff --git a/firefox/safaribooks/zip.js b/firefox/safaribooks/zip.js new file mode 100644 index 0000000..0bc744e --- /dev/null +++ b/firefox/safaribooks/zip.js @@ -0,0 +1,151 @@ +const CRC_TABLE = (() => { + const table = new Uint32Array(256); + for (let i = 0; i < 256; i++) { + let c = i; + for (let j = 0; j < 8; j++) { + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + } + table[i] = c >>> 0; + } + return table; +})(); + +function crc32(data) { + let crc = 0xffffffff; + for (let i = 0; i < data.length; i++) { + const byte = data[i]; + crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); + } + return (crc ^ 0xffffffff) >>> 0; +} + +const textEncoder = new TextEncoder(); + +export class SimpleZip { + constructor() { + this.files = []; + } + + addFile(path, data) { + if (!(data instanceof Uint8Array)) { + throw new TypeError(`File data for ${path} must be a Uint8Array.`); + } + const cleanPath = path.replace(/\\/g, "/"); + const nameBytes = textEncoder.encode(cleanPath); + const checksum = crc32(data); + this.files.push({ + name: cleanPath, + nameBytes, + data, + crc: checksum, + localHeaderOffset: 0 + }); + } + + generate() { + let size = 0; + for (const file of this.files) { + const localSize = 30 + file.nameBytes.length + file.data.length; + size += localSize; + } + + const centralDirectoryOffset = size; + let centralDirectorySize = 0; + for (const file of this.files) { + centralDirectorySize += 46 + file.nameBytes.length; + } + size += centralDirectorySize + 22; + + const output = new Uint8Array(size); + const view = new DataView(output.buffer); + let offset = 0; + + for (const file of this.files) { + file.localHeaderOffset = offset; + view.setUint32(offset, 0x04034b50, true); + offset += 4; + view.setUint16(offset, 20, true); // version needed to extract + offset += 2; + view.setUint16(offset, 0, true); // general purpose flag + offset += 2; + view.setUint16(offset, 0, true); // compression (store) + offset += 2; + view.setUint16(offset, 0, true); // mod time + offset += 2; + view.setUint16(offset, 0, true); // mod date + offset += 2; + view.setUint32(offset, file.crc, true); + offset += 4; + view.setUint32(offset, file.data.length, true); + offset += 4; + view.setUint32(offset, file.data.length, true); + offset += 4; + view.setUint16(offset, file.nameBytes.length, true); + offset += 2; + view.setUint16(offset, 0, true); // extra length + offset += 2; + output.set(file.nameBytes, offset); + offset += file.nameBytes.length; + output.set(file.data, offset); + offset += file.data.length; + } + + let centralOffset = centralDirectoryOffset; + for (const file of this.files) { + view.setUint32(centralOffset, 0x02014b50, true); + centralOffset += 4; + view.setUint16(centralOffset, 0x0014, true); // version made by + centralOffset += 2; + view.setUint16(centralOffset, 20, true); // version needed to extract + centralOffset += 2; + view.setUint16(centralOffset, 0, true); // general purpose flag + centralOffset += 2; + view.setUint16(centralOffset, 0, true); // compression + centralOffset += 2; + view.setUint16(centralOffset, 0, true); // mod time + centralOffset += 2; + view.setUint16(centralOffset, 0, true); // mod date + centralOffset += 2; + view.setUint32(centralOffset, file.crc, true); + centralOffset += 4; + view.setUint32(centralOffset, file.data.length, true); + centralOffset += 4; + view.setUint32(centralOffset, file.data.length, true); + centralOffset += 4; + view.setUint16(centralOffset, file.nameBytes.length, true); + centralOffset += 2; + view.setUint16(centralOffset, 0, true); // extra field length + centralOffset += 2; + view.setUint16(centralOffset, 0, true); // comment length + centralOffset += 2; + view.setUint16(centralOffset, 0, true); // disk number start + centralOffset += 2; + view.setUint16(centralOffset, 0, true); // internal file attrs + centralOffset += 2; + view.setUint32(centralOffset, 0, true); // external file attrs + centralOffset += 4; + view.setUint32(centralOffset, file.localHeaderOffset, true); + centralOffset += 4; + output.set(file.nameBytes, centralOffset); + centralOffset += file.nameBytes.length; + } + + view.setUint32(centralOffset, 0x06054b50, true); + centralOffset += 4; + view.setUint16(centralOffset, 0, true); // number of this disk + centralOffset += 2; + view.setUint16(centralOffset, 0, true); // disk where central directory starts + centralOffset += 2; + view.setUint16(centralOffset, this.files.length, true); // records on this disk + centralOffset += 2; + view.setUint16(centralOffset, this.files.length, true); // total records + centralOffset += 2; + view.setUint32(centralOffset, centralDirectorySize, true); + centralOffset += 4; + view.setUint32(centralOffset, centralDirectoryOffset, true); + centralOffset += 4; + view.setUint16(centralOffset, 0, true); // comment length + + return output; + } +} diff --git a/scripts/build-xpi.ps1 b/scripts/build-xpi.ps1 new file mode 100644 index 0000000..225e514 --- /dev/null +++ b/scripts/build-xpi.ps1 @@ -0,0 +1,24 @@ +param( + [string]$Source = "firefox", + [string]$Output = "firefox-extension.xpi" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Resolve-Path (Join-Path $scriptRoot "..") +$sourcePath = Join-Path $repoRoot $Source +$outputPath = Join-Path $repoRoot $Output + +if (-not (Test-Path -Path $sourcePath -PathType Container)) { + throw "Source folder '$sourcePath' was not found." +} + +if (Test-Path -Path $outputPath -PathType Leaf) { + Remove-Item -Path $outputPath -Force +} + +Compress-Archive -Path (Join-Path $sourcePath "*") -DestinationPath $outputPath -Force + +Write-Host "Created XPI at $outputPath" diff --git a/scripts/build-xpi.sh b/scripts/build-xpi.sh new file mode 100644 index 0000000..3271732 --- /dev/null +++ b/scripts/build-xpi.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +SOURCE_DIR="${1:-firefox}" +OUTPUT_FILE="${2:-firefox-extension.xpi}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +SOURCE_PATH="${REPO_ROOT}/${SOURCE_DIR}" +OUTPUT_PATH="${REPO_ROOT}/${OUTPUT_FILE}" + +if [[ ! -d "${SOURCE_PATH}" ]]; then + echo "Source folder '${SOURCE_PATH}' was not found." >&2 + exit 1 +fi + +if ! command -v zip >/dev/null 2>&1; then + echo "The 'zip' command is required but was not found in PATH." >&2 + exit 1 +fi + +rm -f "${OUTPUT_PATH}" + +( + cd "${SOURCE_PATH}" + zip -r "${OUTPUT_PATH}" . >/dev/null +) + +echo "Created XPI at ${OUTPUT_PATH}"