UNPKG

@vivliostyle/cli

Version:

Save the pdf file via headless browser and Vivliostyle.

1,689 lines (1,676 loc) 120 kB
import { importNodeModule } from "./chunk-FXUEYQRY.js"; import { DetailError, Logger, assertPubManifestSchema, cwd, getDefaultEpubOpfPath, getEpubRootDir, getFormattedError, isInContainer, isRunningOnWSL, isValidUri, openEpub, parseJsonc, pathContains, pathEquals, prettifySchemaError, readJSON, registerExitHandler, runExitHandlers, setupConfigFromFlags, statFileSync, touchTmpFile, useTmpDirectory, writeFileIfChanged } from "./chunk-HCZKJQUX.js"; import { VivliostyleConfigSchema } from "./chunk-YUYXQJDY.js"; import { CONTAINER_LOCAL_HOSTNAME, CONTAINER_URL, COVER_HTML_FILENAME, COVER_HTML_IMAGE_ALT, EMPTY_DATA_URI, EPUB_CONTAINER_XML, EPUB_LANDMARKS_COVER_ENTRY, EPUB_LANDMARKS_TITLE, EPUB_LANDMARKS_TOC_ENTRY, EPUB_NS, EPUB_OUTPUT_VERSION, MANIFEST_FILENAME, TOC_FILENAME, TOC_TITLE, VIEWER_ROOT_PATH, XML_DECLARATION, cliVersion, viewerRoot } from "./chunk-4IIM6RSG.js"; import { __callDispose, __using } from "./chunk-I7BWSAN6.js"; // src/config/load.ts import fs from "node:fs"; import { createRequire } from "node:module"; import { pathToFileURL } from "node:url"; import upath from "upath"; import * as v from "valibot"; var require2 = createRequire(import.meta.url); function locateVivliostyleConfig({ config, cwd: cwd2 = cwd }) { if (config) { return upath.resolve(cwd2, config); } return [".js", ".mjs", ".cjs", ".json"].map((ext) => upath.join(cwd2, `vivliostyle.config${ext}`)).find((p) => fs.existsSync(p)); } async function loadVivliostyleConfig({ config, configData, cwd: cwd2 }) { if (configData) { return v.parse(VivliostyleConfigSchema, configData); } const absPath = locateVivliostyleConfig({ config, cwd: cwd2 }); if (!absPath) { return; } let parsedConfig; let jsonRaw; try { if (upath.extname(absPath) === ".json") { jsonRaw = fs.readFileSync(absPath, "utf8"); parsedConfig = parseJsonc(jsonRaw); } else { delete require2.cache[require2.resolve(absPath)]; const url = pathToFileURL(absPath); url.search = `version=${Date.now()}`; parsedConfig = (await import( /* @vite-ignore */ url.href )).default; jsonRaw = JSON.stringify(parsedConfig, null, 2); } } catch (error) { const thrownError = error; throw new DetailError( `An error occurred on loading a config file: ${absPath}`, thrownError.stack ?? thrownError.message ); } const result = v.safeParse(VivliostyleConfigSchema, parsedConfig); if (result.success) { const { tasks, inlineOptions } = result.output; return { tasks, inlineOptions: { ...inlineOptions, cwd: cwd2 ?? cwd, config: absPath } }; } else { const errorString = prettifySchemaError(jsonRaw, result.issues); throw new DetailError( `Validation of vivliostyle config failed. Please check the schema: ${config}`, errorString ); } } function warnDeprecatedConfig(config) { if (config.tasks.some((task) => task.includeAssets)) { Logger.logWarn( "'includeAssets' property of Vivliostyle config was deprecated and will be removed in a future release. Please use 'copyAsset.includes' property instead." ); } if (config.tasks.some((task) => task.tocTitle)) { Logger.logWarn( "'tocTitle' property of Vivliostyle config was deprecated and will be removed in a future release. Please use 'toc.title' property instead." ); } if (config.tasks.some((task) => task.http)) { Logger.logWarn( "'http' property of Vivliostyle config was deprecated and will be removed in a future release. This option is enabled by default, and the file protocol is no longer supported." ); } } // src/config/merge.ts var pruneObject = (obj) => { const ret = { ...obj }; for (const key in ret) { if (ret[key] === void 0 || ret[key] === null) { delete ret[key]; } } return ret; }; function mergeConfig(base, override) { return { tasks: base.tasks.map((task, i) => ({ ...pruneObject(task), ...pruneObject(override) })), inlineOptions: base.inlineOptions }; } function mergeInlineConfig({ tasks, inlineOptions }, inlineConfig) { const { theme, size, pressReady, title, author, language, readingProgression, timeout, image, viewer, viewerParam, browser, output, renderMode, preflight, preflightOption, vite, viteConfigFile, host, port, ...overrideInlineOptions } = inlineConfig; return { tasks: tasks.map((task) => ({ ...pruneObject(task), ...pruneObject({ theme, size, pressReady, title, author, language, readingProgression, timeout, image, viewer, viewerParam, browser, vite, viteConfigFile }), output: (output?.length ? output : task.output)?.map((o) => ({ ...pruneObject(o), ...pruneObject({ renderMode, preflight, preflightOption }) })), server: { ...pruneObject(task.server ?? {}), ...pruneObject({ host, port }) } })), inlineOptions: { ...pruneObject(inlineOptions), ...pruneObject( overrideInlineOptions ) } }; } // src/config/resolve.ts import { VFM } from "@vivliostyle/vfm"; import { lookup as mime } from "mime-types"; import fs3 from "node:fs"; import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url"; import npa from "npm-package-arg"; import upath2 from "upath"; // src/processor/markdown.ts import { readMetadata } from "@vivliostyle/vfm"; import fs2 from "node:fs"; import vfile from "vfile"; function safeReadMetadata(content) { try { return readMetadata(content); } catch { return {}; } } async function processMarkdown(documentProcessorFactory, filepath, options = {}) { const markdownString = fs2.readFileSync(filepath, "utf8"); const processor = documentProcessorFactory( options, safeReadMetadata(markdownString) ); const processed = await processor.process( vfile({ path: filepath, contents: markdownString }) ); return processed; } function readMarkdownMetadata(filepath) { return safeReadMetadata(fs2.readFileSync(filepath, "utf8")); } // src/config/resolve.ts var manuscriptMediaTypes = [ "text/markdown", "text/html", "application/xhtml+xml" ]; var UseTemporaryServerRoot = Symbol("UseTemporaryServerRoot"); var DEFAULT_ASSET_EXTENSIONS = [ "css", "css.map", "png", "jpg", "jpeg", "svg", "gif", "webp", "apng", "ttf", "otf", "woff", "woff2" ]; function isManuscriptMediaType(mediaType) { return !!(mediaType && manuscriptMediaTypes.includes(mediaType)); } function isWebPubConfig(config) { return config.viewerInput.type === "webpub"; } function isWebbookConfig(config) { return config.viewerInput.type === "webbook"; } function parsePackageName(specifier, cwd2) { try { let result = npa(specifier, cwd2); if (result.type === "git" && result.saveSpec?.startsWith("github:")) { result = npa(`file:${specifier}`, cwd2); } return result; } catch (error) { return null; } } function parseTheme({ theme, context, workspaceDir, themesDir }) { const { specifier, import: importPath } = typeof theme === "string" ? { specifier: theme, import: void 0 } : theme; if (isValidUri(specifier)) { return { type: "uri", name: upath2.basename(specifier), location: specifier }; } const stylePath = upath2.resolve(context, specifier); if (fs3.existsSync(stylePath) && stylePath.endsWith(".css")) { const sourceRelPath = upath2.relative(context, stylePath); return { type: "file", name: upath2.basename(specifier), source: stylePath, location: upath2.resolve(workspaceDir, sourceRelPath) }; } const parsed = parsePackageName(specifier, context); if (!parsed) { throw new Error(`Invalid package name: ${specifier}`); } if (!parsed.registry && parsed.type !== "directory") { throw new Error(`This package specifier is not allowed: ${specifier}`); } let name = parsed.name; let resolvedSpecifier = specifier; if (parsed.type === "directory" && parsed.fetchSpec) { const pkgJsonPath = upath2.join(parsed.fetchSpec, "package.json"); if (fs3.existsSync(pkgJsonPath)) { const packageJson = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf8")); name = packageJson.name; resolvedSpecifier = parsed.fetchSpec; } } if (!name) { throw new Error(`Could not determine the package name: ${specifier}`); } return { type: "package", name, specifier: resolvedSpecifier, location: upath2.join(themesDir, "node_modules", name), registry: Boolean(parsed.registry), importPath }; } function parsePageSize(size) { const [width, height, ...others] = `${size}`.split(","); if (!width || others.length) { throw new Error(`Cannot parse size: ${size}`); } else if (width && height) { return { width, height }; } else { return { format: width }; } } function parseFileMetadata({ contentType, sourcePath, workspaceDir, themesDir }) { const sourceDir = upath2.dirname(sourcePath); let title; let themes; if (contentType === "text/markdown") { const metadata = readMarkdownMetadata(sourcePath); title = metadata.title; if (metadata.vfm?.theme && themesDir) { themes = [metadata.vfm.theme].flat().filter( (entry) => !!entry && (typeof entry === "string" || typeof entry === "object") ).map( (theme) => parseTheme({ theme, context: sourceDir, workspaceDir, themesDir }) ); } } else { const content = fs3.readFileSync(sourcePath, "utf8"); title = content.match(/<title>([^<]*)<\/title>/)?.[1] || void 0; } return { title, themes }; } function parseCustomStyle({ customStyle, entryContextDir }) { if (isValidUri(customStyle)) { return customStyle; } const stylePath = upath2.resolve(entryContextDir, customStyle); if (!pathContains(entryContextDir, stylePath)) { throw Error( `Custom style file ${customStyle} is not in ${entryContextDir}. Make sure the file is located in the context directory or a subdirectory.` ); } if (!fs3.existsSync(stylePath)) { throw new Error(`Custom style file not found: ${customStyle}`); } return pathToFileURL2(stylePath).href.slice( pathToFileURL2(entryContextDir).href.replace(/\/$/, "").length + 1 ); } function resolveTaskConfig(config, options) { const context = options.cwd ?? cwd; Logger.debug("resolveTaskConfig > context %s", context); const entryContextDir = config.entryContext ? upath2.resolve(context, config.entryContext) : context; const language = config.language; const readingProgression = config.readingProgression; const size = config.size ? parsePageSize(config.size) : void 0; const cropMarks = options.cropMarks ?? false; const bleed = options.bleed; const cropOffset = options.cropOffset; const css = options.css; const singleDoc = options.singleDoc ?? false; const quick = options.quick ?? false; const temporaryFilePrefix = config.temporaryFilePrefix ?? `.vs-${Date.now()}.`; const documentProcessorFactory = config?.documentProcessor ?? VFM; const vfmOptions = { ...config?.vfm, hardLineBreaks: config?.vfm?.hardLineBreaks ?? false, disableFormatHtml: config?.vfm?.disableFormatHtml ?? false }; const timeout = config.timeout ?? 3e5; const sandbox = options.sandbox ?? false; const browser = { type: config.browser ?? "chromium", executablePath: options.executableBrowser }; const proxyServer = options.proxyServer ?? process.env.HTTP_PROXY ?? void 0; const proxy = proxyServer ? { server: proxyServer, bypass: options.proxyBypass ?? process.env.NOPROXY ?? void 0, username: options.proxyUser, password: options.proxyPass } : void 0; const image = config.image ?? `${CONTAINER_URL}:${cliVersion}`; const viewer = config.viewer ?? void 0; const viewerParam = config.viewerParam ?? void 0; const logLevel = options.logLevel ?? "silent"; const ignoreHttpsErrors = options.ignoreHttpsErrors ?? false; const base = config.base ?? "/vivliostyle"; const staticRoutes = config.static ?? {}; const viteConfig = config.vite; const viteConfigFile = config.viteConfigFile ?? true; const customStyle = options.style && parseCustomStyle({ customStyle: options.style, entryContextDir }) || void 0; const customUserStyle = options.userStyle && parseCustomStyle({ customStyle: options.userStyle, entryContextDir }) || void 0; const outputs = (() => { const defaultPdfOptions = { format: "pdf", renderMode: options.renderMode ?? "local", preflight: options.preflight ?? (config.pressReady ? "press-ready" : void 0), preflightOption: options.preflightOption ?? [] }; if (config.output) { return config.output.map((target) => { const outputPath = upath2.resolve(context, target.path); const format = target.format; switch (format) { case "pdf": return { ...defaultPdfOptions, ...target, format, path: outputPath }; case "epub": return { ...target, format, path: outputPath, version: EPUB_OUTPUT_VERSION }; case "webpub": return { ...target, format, path: outputPath }; default: return format; } }); } const filename = config.title ? `${config.title}.pdf` : "output.pdf"; return [ { ...defaultPdfOptions, path: upath2.resolve(context, filename) } ]; })(); const { server, rootUrl } = (() => { let host = config.server?.host ?? false; let allowedHosts = config.server?.allowedHosts || []; const port = config.server?.port ?? 13e3; if (outputs.some( (target) => target.format === "pdf" && target.renderMode === "docker" ) && !isInContainer()) { host = true; if (Array.isArray(allowedHosts) && !allowedHosts.includes(CONTAINER_LOCAL_HOSTNAME)) { allowedHosts.push(CONTAINER_LOCAL_HOSTNAME); } } const rootHostname = !host ? "localhost" : host === true ? "0.0.0.0" : host; return { server: { host, port, proxy: config.server?.proxy ?? {}, allowedHosts }, rootUrl: `http://${rootHostname}:${port}` }; })(); const cover = config.cover && { src: upath2.resolve(entryContextDir, config.cover.src), name: config.cover.name || COVER_HTML_IMAGE_ALT }; const copyAsset = { includes: config.copyAsset?.includes ?? config.includeAssets ?? [], excludes: config.copyAsset?.excludes ?? [], fileExtensions: [ .../* @__PURE__ */ new Set([ ...DEFAULT_ASSET_EXTENSIONS, ...config.copyAsset?.includeFileExtensions ?? [] ]) ].filter( (ext) => !(config.copyAsset?.excludeFileExtensions ?? []).includes(ext) ) }; const themeIndexes = /* @__PURE__ */ new Set(); const projectConfig = !options.config && options.input ? resolveSingleInputConfig({ config, input: options.input, context, temporaryFilePrefix, themeIndexes, base }) : resolveComposedProjectConfig({ config, context, entryContextDir, outputs, temporaryFilePrefix, themeIndexes, cover }); for (const output of outputs) { const relPath = upath2.relative(context, output.path); if (pathContains(output.path, entryContextDir) || pathEquals(output.path, entryContextDir)) { throw new Error( `The output path is set to "${relPath}", but this will overwrite the original manuscript file. Please specify a different path.` ); } if (pathContains(output.path, projectConfig.workspaceDir) || pathEquals(output.path, projectConfig.workspaceDir)) { throw new Error( `The output path is set to "${relPath}", but this will overwrite the working directory of Vivliostyle. Please specify a different path.` ); } } const { entries, workspaceDir } = projectConfig; const duplicatedTarget = entries.find( (v1, i) => entries.findLastIndex((v2) => v1.target === v2.target) !== i )?.target; if (duplicatedTarget) { const sourceFile = entries.find( (entry) => entry.target === duplicatedTarget && entry.source?.type === "file" )?.source; throw new Error( `The output path "${upath2.relative(workspaceDir, duplicatedTarget)}" will overwrite existing content.` + (sourceFile ? ` Please choose a different name for the source file: ${sourceFile.pathname}` : "") ); } const resolvedConfig = { ...projectConfig, entryContextDir, outputs, themeIndexes, copyAsset, temporaryFilePrefix, size, cropMarks, bleed, cropOffset, css, customStyle, customUserStyle, singleDoc, quick, language, readingProgression, documentProcessorFactory, vfmOptions, cover, timeout, sandbox, browser, proxy, image, viewer, viewerParam, logLevel, ignoreHttpsErrors, base, server, static: staticRoutes, rootUrl, viteConfig, viteConfigFile }; return resolvedConfig; } function resolveSingleInputConfig({ config, input, context, temporaryFilePrefix, themeIndexes, base }) { Logger.debug("entering single entry config mode"); let serverRootDir; let sourcePath; let workspaceDir; const inputFormat = input.format; const title = config?.title; const author = config?.author; const entries = []; const exportAliases = []; let isLocalResource = true; if (isValidUri(input.entry)) { const url = new URL(input.entry); if (url.protocol === "file:") { sourcePath = fileURLToPath(url); } else { isLocalResource = false; sourcePath = input.entry; } } else { sourcePath = upath2.resolve(context, input.entry); } if (isLocalResource) { statFileSync(sourcePath); switch (input.format) { case "webbook": case "markdown": case "pub-manifest": case "epub": workspaceDir = upath2.dirname(sourcePath); break; case "epub-opf": { const rootDir = getEpubRootDir(sourcePath); if (!rootDir) { throw new Error( `Could not determine the EPUB root directory for the OPF file: ${sourcePath}` ); } workspaceDir = rootDir; break; } default: return input.format; } serverRootDir = workspaceDir; } else { serverRootDir = UseTemporaryServerRoot; workspaceDir = context; } const themesDir = upath2.resolve(workspaceDir, "themes"); if (input.format === "markdown") { const contentType = "text/markdown"; const metadata = parseFileMetadata({ contentType, sourcePath, workspaceDir }); const target = upath2.resolve( workspaceDir, `${temporaryFilePrefix}${upath2.basename(sourcePath)}` ).replace(/\.md$/, ".html"); touchTmpFile(target); const themes = metadata.themes ?? config.theme?.map( (theme) => parseTheme({ theme, context, workspaceDir, themesDir }) ) ?? []; themes.forEach((t) => themeIndexes.add(t)); entries.push({ contentType, source: { type: "file", pathname: sourcePath, contentType }, target, title: metadata.title, themes }); exportAliases.push({ source: target, target: upath2.resolve( upath2.dirname(target), upath2.basename(sourcePath).replace(/\.md$/, ".html") ) }); } let fallbackTitle; let viewerInput; if (inputFormat === "markdown") { const manifestPath = upath2.resolve( workspaceDir, `${temporaryFilePrefix}${MANIFEST_FILENAME}` ); touchTmpFile(manifestPath); exportAliases.push({ source: manifestPath, target: upath2.resolve(workspaceDir, MANIFEST_FILENAME) }); fallbackTitle = entries.length === 1 && entries[0].title ? entries[0].title : upath2.basename(sourcePath); viewerInput = { type: "webpub", manifestPath, needToGenerateManifest: true }; } else if (inputFormat === "webbook") { let webbookEntryUrl; let webbookPath; if (isValidUri(sourcePath)) { const url = new URL(sourcePath); webbookEntryUrl = url.href; } else { const rootFileUrl = pathToFileURL2(workspaceDir).href; const urlPath = pathToFileURL2(sourcePath).href.slice(rootFileUrl.length); webbookEntryUrl = `${base}${urlPath}`; webbookPath = sourcePath; } viewerInput = { type: "webbook", webbookEntryUrl, webbookPath }; } else if (inputFormat === "pub-manifest") { viewerInput = { type: "webpub", manifestPath: sourcePath, needToGenerateManifest: false }; } else if (inputFormat === "epub-opf") { viewerInput = { type: "epub-opf", epubOpfPath: sourcePath }; } else if (inputFormat === "epub") { viewerInput = { type: "epub", epubPath: sourcePath, epubTmpOutputDir: upath2.join( sourcePath, `../${temporaryFilePrefix}${upath2.basename(sourcePath)}` ) }; } else { return inputFormat; } return { serverRootDir, workspaceDir, themesDir, entries, input: { format: inputFormat, entry: sourcePath }, viewerInput, exportAliases, title: title || fallbackTitle, author }; } function resolveComposedProjectConfig({ config, context, entryContextDir, outputs, temporaryFilePrefix, themeIndexes, cover }) { Logger.debug("entering composed project config mode"); const workspaceDir = upath2.resolve( context, config.workspaceDir ?? ".vivliostyle" ); const themesDir = upath2.resolve(workspaceDir, "themes"); const pkgJsonPath = upath2.resolve(context, "package.json"); const pkgJson = fs3.existsSync(pkgJsonPath) ? readJSON(pkgJsonPath) : void 0; if (pkgJson) { Logger.debug("located package.json path", pkgJsonPath); } const exportAliases = []; const rootThemes = config.theme?.map( (theme) => parseTheme({ theme, context, workspaceDir, themesDir }) ) ?? []; rootThemes.forEach((t) => themeIndexes.add(t)); const tocConfig = { tocTitle: config.toc?.title ?? config?.tocTitle ?? TOC_TITLE, target: upath2.resolve(workspaceDir, config.toc?.htmlPath ?? TOC_FILENAME), sectionDepth: config.toc?.sectionDepth ?? 0, transform: { transformDocumentList: config.toc?.transformDocumentList, transformSectionList: config.toc?.transformSectionList } }; const coverHtml = config.cover && ("htmlPath" in config.cover && !config.cover.htmlPath ? void 0 : upath2.resolve( workspaceDir, config.cover?.htmlPath || COVER_HTML_FILENAME )); const ensureCoverImage = (src) => { const absPath = src && upath2.resolve(entryContextDir, src); if (absPath) { statFileSync(absPath, { errorMessage: "Specified cover image does not exist" }); } return absPath; }; const projectTitle = config?.title ?? pkgJson?.name; const projectAuthor = config?.author ?? pkgJson?.author; const isContentsEntry = (entry) => entry.rel === "contents"; const isCoverEntry = (entry) => entry.rel === "cover"; const isArticleEntry = (entry) => !isContentsEntry(entry) && !isCoverEntry(entry); function parseEntry(entry) { const getInputInfo = (entryPath) => { if (/^https?:/.test(entryPath)) { return { type: "uri", href: entryPath, rootDir: upath2.join(workspaceDir, new URL(entryPath).host) }; } else if (entryPath.startsWith("/")) { return { type: "uri", href: entryPath, rootDir: upath2.join(workspaceDir, "localhost") }; } const pathname = upath2.resolve(entryContextDir, entryPath); statFileSync(pathname); const contentType = mime(pathname); if (!isManuscriptMediaType(contentType)) { throw new Error( `Invalid manuscript type ${contentType} detected: ${entry}` ); } return { type: "file", pathname, contentType, metadata: parseFileMetadata({ contentType, sourcePath: pathname, workspaceDir, themesDir }) }; }; const getTargetPath = (source) => { switch (source.type) { case "file": return upath2.resolve( workspaceDir, upath2.relative(entryContextDir, source.pathname).replace(/\.md$/, ".html") ); case "uri": { const url = new URL(source.href, "a://dummy"); let pathname = url.pathname; if (!/\.\w+$/.test(pathname)) { pathname = `${pathname.replace(/\/$/, "")}/index.html`; } return upath2.join(source.rootDir, pathname); } default: return source; } }; if ((isContentsEntry(entry) || isCoverEntry(entry)) && entry.path) { const source = upath2.resolve(entryContextDir, entry.path); try { statFileSync(source); } catch (error) { Logger.logWarn( `The "path" option is set but the file does not exist: ${source} Maybe you want to set the "output" field instead.` ); entry.output = entry.path; entry.path = void 0; } } if (isContentsEntry(entry)) { const inputInfo = entry.path ? getInputInfo(entry.path) : void 0; const { metadata, ...template } = inputInfo || {}; let target = entry.output ? upath2.resolve(workspaceDir, entry.output) : inputInfo && getTargetPath(inputInfo); const themes = entry.theme ? [entry.theme].flat().map( (theme) => parseTheme({ theme, context, workspaceDir, themesDir }) ) : metadata?.themes ?? [...rootThemes]; themes.forEach((t) => themeIndexes.add(t)); target ??= tocConfig.target; if (inputInfo?.type === "file" && pathEquals(inputInfo.pathname, target)) { const tmpPath = upath2.resolve( upath2.dirname(target), `${temporaryFilePrefix}${upath2.basename(target)}` ); exportAliases.push({ source: tmpPath, target }); touchTmpFile(tmpPath); target = tmpPath; } const parsedEntry = { rel: "contents", ...tocConfig, target, title: entry.title ?? metadata?.title ?? projectTitle, themes, pageBreakBefore: entry.pageBreakBefore, pageCounterReset: entry.pageCounterReset, ..."type" in template && { template } }; return parsedEntry; } if (isCoverEntry(entry)) { const inputInfo = entry.path ? getInputInfo(entry.path) : void 0; const { metadata, ...template } = inputInfo || {}; let target = entry.output ? upath2.resolve(workspaceDir, entry.output) : inputInfo && getTargetPath(inputInfo); const themes = entry.theme ? [entry.theme].flat().map( (theme) => parseTheme({ theme, context, workspaceDir, themesDir }) ) : metadata?.themes ?? []; themes.forEach((t) => themeIndexes.add(t)); const coverImageSrc = ensureCoverImage(entry.imageSrc || cover?.src); if (!coverImageSrc) { throw new Error( `A CoverEntryConfig is set in the entry list but a location of cover file is not set. Please set 'cover' property in your config file.` ); } target ??= upath2.resolve( workspaceDir, entry.path || coverHtml || COVER_HTML_FILENAME ); if (inputInfo?.type === "file" && pathEquals(inputInfo.pathname, target)) { const tmpPath = upath2.resolve( upath2.dirname(target), `${temporaryFilePrefix}${upath2.basename(target)}` ); exportAliases.push({ source: tmpPath, target }); touchTmpFile(tmpPath); target = tmpPath; } const parsedEntry = { rel: "cover", target, title: entry.title ?? metadata?.title ?? projectTitle, themes, coverImageSrc, coverImageAlt: entry.imageAlt || cover?.name || COVER_HTML_IMAGE_ALT, pageBreakBefore: entry.pageBreakBefore, ..."type" in template && { template } }; return parsedEntry; } if (isArticleEntry(entry)) { const inputInfo = getInputInfo(entry.path); const { metadata, ...source } = inputInfo; const target = entry.output ? upath2.resolve(workspaceDir, entry.output) : getTargetPath(inputInfo); const themes = entry.theme ? [entry.theme].flat().map( (theme) => parseTheme({ theme, context, workspaceDir, themesDir }) ) : metadata?.themes ?? [...rootThemes]; themes.forEach((t) => themeIndexes.add(t)); const parsedEntry = { contentType: inputInfo.type === "file" ? inputInfo.contentType : "text/html", source, target, title: entry.title ?? metadata?.title ?? projectTitle, themes, ...entry.rel && { rel: entry.rel } }; return parsedEntry; } return entry; } const entries = config.entry.map(parseEntry); let fallbackProjectTitle; if (!projectTitle) { if (entries.length === 1 && entries[0].title) { fallbackProjectTitle = entries[0].title; } else { fallbackProjectTitle = upath2.basename(outputs[0].path); } } if (!!config?.toc && !entries.find(({ rel }) => rel === "contents")) { entries.unshift({ rel: "contents", ...tocConfig, themes: [...rootThemes] }); } if (cover && coverHtml && !entries.find(({ rel }) => rel === "cover")) { entries.unshift({ rel: "cover", target: coverHtml, title: projectTitle, themes: [], // Don't inherit rootThemes for cover documents coverImageSrc: ensureCoverImage(cover.src), coverImageAlt: cover.name }); } return { serverRootDir: context, workspaceDir, themesDir, entries, input: { format: "pub-manifest", entry: upath2.join(workspaceDir, MANIFEST_FILENAME) }, viewerInput: { type: "webpub", manifestPath: upath2.join(workspaceDir, MANIFEST_FILENAME), needToGenerateManifest: true }, exportAliases, title: projectTitle || fallbackProjectTitle, author: projectAuthor }; } // src/browser.ts import fs4 from "node:fs"; async function launchBrowser({ browserType, proxy, executablePath, headless, noSandbox, disableDevShmUsage }) { const playwright = await importNodeModule("playwright-core"); playwright.firefox.executablePath; const options = browserType === "chromium" ? { executablePath, chromiumSandbox: !noSandbox, headless, args: [ // #579: disable web security to allow cross-origin requests "--disable-web-security", ...disableDevShmUsage ? ["--disable-dev-shm-usage"] : [], // #357: Set devicePixelRatio=1 otherwise it causes layout issues in HiDPI displays ...headless ? ["--force-device-scale-factor=1"] : [], // #565: Add --disable-gpu option when running on WSL ...isRunningOnWSL() ? ["--disable-gpu"] : [], // set Chromium language to English to avoid locale-dependent issues "--lang=en", ...!headless && process.platform === "darwin" ? ["", "-AppleLanguages", "(en)"] : [] ], env: { ...process.env, LANG: "en.UTF-8" }, proxy } : ( // TODO: Investigate appropriate settings on Firefox & Webkit { executablePath, headless } ); const browser = await playwright[browserType].launch(options); registerExitHandler("Closing browser", () => { browser.close(); }); return browser; } async function getExecutableBrowserPath(browserType) { const playwright = await importNodeModule("playwright-core"); return playwright[browserType].executablePath(); } function getFullBrowserName(browserType) { return { chromium: "Chromium", firefox: "Firefox", webkit: "Webkit" }[browserType]; } function checkBrowserAvailability(path) { return fs4.existsSync(path); } async function downloadBrowser(browserType) { const { registry } = await importNodeModule("playwright-core/lib/server"); const executable = registry.findExecutable(browserType); { var _stack = []; try { const _2 = __using(_stack, Logger.suspendLogging( "Rendering browser is not installed yet. Downloading now." )); await registry.install([executable], false); } catch (_) { var _error = _, _hasError = true; } finally { __callDispose(_stack, _error, _hasError); } } return executable.executablePath(); } async function launchPreview({ mode, url, onBrowserOpen, onPageOpen, config: { browser: browserConfig, proxy, sandbox, ignoreHttpsErrors } }) { let executableBrowser = browserConfig.executablePath; if (executableBrowser) { if (!checkBrowserAvailability(executableBrowser)) { throw new Error( `Cannot find the browser. Please check the executable browser path: ${executableBrowser}` ); } } else { executableBrowser = await getExecutableBrowserPath(browserConfig.type); if (!checkBrowserAvailability(executableBrowser)) { await downloadBrowser(browserConfig.type); } } Logger.debug(`Executing browser path: ${executableBrowser}`); const browser = await launchBrowser({ browserType: browserConfig.type, proxy, executablePath: executableBrowser, headless: mode === "build", noSandbox: !sandbox, disableDevShmUsage: isInContainer() }); await onBrowserOpen?.(browser); const page = await browser.newPage({ viewport: mode === "build" ? ( // This viewport size important to detect headless environment in Vivliostyle viewer // https://github.com/vivliostyle/vivliostyle.js/blob/73bcf323adcad80126b0175630609451ccd09d8a/packages/core/src/vivliostyle/vgen.ts#L2489-L2500 { width: 800, height: 600 } ) : null, ignoreHTTPSErrors: ignoreHttpsErrors }); await onPageOpen?.(page); page.on("dialog", () => { }); await page.goto(url); return { browser, page }; } // src/server.ts import fs11 from "node:fs"; import { URL as URL2 } from "node:url"; import upath11 from "upath"; import { createServer, preview } from "vite"; // src/vite/vite-plugin-dev-server.ts import escapeRe from "escape-string-regexp"; import { pathToFileURL as pathToFileURL6 } from "node:url"; import sirv from "sirv"; import upath8 from "upath"; // src/processor/asset.ts import { copy } from "fs-extra/esm"; import fs5 from "node:fs"; import picomatch from "picomatch"; import { glob } from "tinyglobby"; import upath3 from "upath"; var GlobMatcher = class { constructor(matcherConfig) { this.matcherConfig = matcherConfig; this.#_matchers = matcherConfig.map( ({ patterns, ...options }) => picomatch(patterns, options) ); } #_matchers; match(test) { return this.#_matchers.some((matcher) => matcher(test)); } async glob(globOptions = {}) { return new Set( (await Promise.all( this.matcherConfig.map( (config) => glob({ ...config, ...globOptions }) ) )).flat() ); } }; function getIgnoreThemeDirectoryPatterns({ themesDir, cwd: cwd2 }) { return pathContains(cwd2, themesDir) ? [ `${upath3.relative(cwd2, themesDir)}/node_modules/*/example`, `${upath3.relative(cwd2, themesDir)}/node_modules/*/*/example` ] : []; } function getIgnoreAssetPatterns({ outputs, entries, cwd: cwd2 }) { return [ ...outputs.flatMap( ({ format, path: p }) => !pathContains(cwd2, p) ? [] : format === "webpub" ? upath3.join(upath3.relative(cwd2, p), "**") : upath3.relative(cwd2, p) ), ...entries.flatMap(({ template }) => { return template?.type === "file" && pathContains(cwd2, template.pathname) ? upath3.relative(cwd2, template.pathname) : []; }) ]; } function getWebPubResourceMatcher({ outputs, themesDir, entries, cwd: cwd2, manifestPath }) { return new GlobMatcher([ { patterns: [ `**/${upath3.relative(cwd2, manifestPath)}`, "**/*.{html,htm,xhtml,xht,css}" ], ignore: [ ...getIgnoreAssetPatterns({ cwd: cwd2, outputs, entries }), ...getIgnoreThemeDirectoryPatterns({ cwd: cwd2, themesDir }), // Ignore node_modules in the root directory "node_modules/**", // only include dotfiles starting with `.vs-` "**/.!(vs-*)/**" ], dot: true, cwd: cwd2 } ]); } function getAssetMatcher({ copyAsset: { fileExtensions, includes, excludes }, outputs, themesDir, entries, cwd: cwd2, ignore = [] }) { const ignorePatterns = [ ...ignore, ...excludes, ...getIgnoreAssetPatterns({ outputs, entries, cwd: cwd2 }) ]; return new GlobMatcher([ // Step 1: Glob files with an extension in `fileExtension` // Ignore files in node_modules directory, theme example files and files matched `excludes` { patterns: fileExtensions.map((ext) => `**/*.${ext}`), ignore: [ "**/node_modules/**", ...ignorePatterns, ...getIgnoreThemeDirectoryPatterns({ themesDir, cwd: cwd2 }) ], cwd: cwd2 }, // Step 2: Glob files matched with `includes` // Ignore only files matched `excludes` { patterns: includes, ignore: ignorePatterns, cwd: cwd2 } ]); } async function copyAssets({ entryContextDir, workspaceDir, copyAsset, outputs, themesDir, entries }) { if (pathEquals(entryContextDir, workspaceDir)) { return; } const relWorkspaceDir = upath3.relative(entryContextDir, workspaceDir); const assets = await getAssetMatcher({ copyAsset, cwd: entryContextDir, outputs, themesDir, entries, ignore: [ // don't copy workspace itself ...relWorkspaceDir ? [upath3.join(relWorkspaceDir, "**")] : [] ] }).glob({ followSymbolicLinks: true }); Logger.debug("assets", assets); for (const asset of assets) { const target = upath3.join(workspaceDir, asset); fs5.mkdirSync(upath3.dirname(target), { recursive: true }); await copy(upath3.resolve(entryContextDir, asset), target); } } // src/processor/compile.ts import { copy as copy4, move } from "fs-extra/esm"; import fs9 from "node:fs"; import upath7 from "upath"; import serializeToXml2 from "w3c-xmlserializer"; import MIMEType2 from "whatwg-mimetype"; // src/output/webbook.ts import { copy as copy3 } from "fs-extra/esm"; import { lookup as mime3 } from "mime-types"; import fs7 from "node:fs"; import { pathToFileURL as pathToFileURL5 } from "node:url"; import { glob as glob2 } from "tinyglobby"; import upath6 from "upath"; // src/processor/html.tsx import jsdom, { ResourceLoader as BaseResourceLoader, JSDOM } from "@vivliostyle/jsdom"; import DOMPurify from "dompurify"; import { toHtml } from "hast-util-to-html"; import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL3 } from "node:url"; import upath4 from "upath"; import MIMEType from "whatwg-mimetype"; import { jsx, jsxs } from "hastscript/jsx-runtime"; var createVirtualConsole = (onError) => { const virtualConsole = new jsdom.VirtualConsole(); virtualConsole.on("error", (message) => { Logger.debug("[JSDOM Console] error:", message); }); virtualConsole.on("warn", (message) => { Logger.debug("[JSDOM Console] warn:", message); }); virtualConsole.on("log", (message) => { Logger.debug("[JSDOM Console] log:", message); }); virtualConsole.on("info", (message) => { Logger.debug("[JSDOM Console] info:", message); }); virtualConsole.on("dir", (message) => { Logger.debug("[JSDOM Console] dir:", message); }); virtualConsole.on("jsdomError", (error) => { if (error.message === "Could not parse CSS stylesheet") { return; } onError( new DetailError( "Error occurred when loading content", error.stack ?? error.message ) ); }); return virtualConsole; }; var htmlPurify = DOMPurify( // @ts-expect-error: jsdom.DOMWindow should have trustedTypes property new JSDOM("").window ); var ResourceLoader = class _ResourceLoader extends BaseResourceLoader { static dataUrlOrigin = "http://localhost/"; fetcherMap = /* @__PURE__ */ new Map(); fetch(url, options) { Logger.debug(`[JSDOM] Fetching resource: ${url}`); const fetcher = super.fetch(url, options); if (fetcher) { this.fetcherMap.set(url, fetcher); } return fetcher; } static async saveFetchedResources({ fetcherMap, rootUrl, outputDir, onError }) { const rootHref = rootUrl.startsWith("data:") ? _ResourceLoader.dataUrlOrigin : /^https?:/i.test(rootUrl) ? new URL("/", rootUrl).href : new URL(".", rootUrl).href; const normalizeToLocalPath = (urlString, mimeType) => { let url = new URL(urlString); url.hash = ""; if (mimeType === "text/html" && !/\.html?$/.test(url.pathname)) { url.pathname = `${url.pathname.replace(/\/$/, "")}/index.html`; } let relTarget = upath4.relative(rootHref, url.href); return decodeURI(relTarget); }; const fetchedResources = []; await Promise.allSettled( [...fetcherMap.entries()].flatMap(async ([url, fetcher]) => { if (!url.startsWith(rootHref)) { return []; } return fetcher.then(async (buffer) => { let encodingFormat; try { const contentType = fetcher.response?.headers["content-type"]; if (contentType) { encodingFormat = new MIMEType(contentType).essence; } } catch (e) { } const relTarget = normalizeToLocalPath(url, encodingFormat); const target = upath4.join(outputDir, relTarget); fetchedResources.push({ url: relTarget, encodingFormat }); writeFileIfChanged(target, buffer); }).catch(onError); }) ); return fetchedResources; } }; async function getJsdomFromUrlOrFile({ src, resourceLoader, virtualConsole = createVirtualConsole((error) => { throw error; }) }) { const url = isValidUri(src) ? new URL(src) : pathToFileURL3(src); let dom; if (url.protocol === "http:" || url.protocol === "https:") { dom = await JSDOM.fromURL(src, { virtualConsole, resources: resourceLoader }); } else if (url.protocol === "file:") { if (resourceLoader) { const file = resourceLoader._readFile(fileURLToPath2(url)); resourceLoader.fetcherMap.set(url.href, file); } dom = await JSDOM.fromFile(fileURLToPath2(url), { virtualConsole, resources: resourceLoader, contentType: src.endsWith(".xhtml") || src.endsWith(".xml") ? "application/xhtml+xml; charset=UTF-8" : "text/html; charset=UTF-8" }); } else if (url.protocol === "data:") { const [head, body] = url.href.split(",", 2); const data = decodeURIComponent(body); const buffer = Buffer.from( data, /;base64$/i.test(head) ? "base64" : "utf8" ); const dummyUrl = `${ResourceLoader.dataUrlOrigin}index.html`; if (resourceLoader) { let timeoutId; const promise = new Promise((resolve) => { timeoutId = setTimeout(resolve, 0, buffer); }); promise.abort = () => { if (timeoutId !== void 0) { clearTimeout(timeoutId); } }; resourceLoader.fetcherMap.set(dummyUrl, promise); } dom = new JSDOM(buffer.toString(), { virtualConsole, resources: resourceLoader, contentType: "text/html; charset=UTF-8", url: dummyUrl }); } else { throw new Error(`Unsupported protocol: ${url.protocol}`); } return dom; } function getJsdomFromString({ html, virtualConsole = createVirtualConsole((error) => { throw error; }) }) { return new JSDOM(html, { virtualConsole }); } async function getStructuredSectionFromHtml(htmlPath, href) { const dom = await getJsdomFromUrlOrFile({ src: htmlPath }); const { document } = dom.window; const allHeadings = [...document.querySelectorAll("h1, h2, h3, h4, h5, h6")].filter((el) => { return !el.matches("blockquote *"); }).sort((a, b) => { const position = a.compareDocumentPosition(b); return position & 2 ? 1 : position & 4 ? -1 : 0; }); function traverse(headers) { if (headers.length === 0) { return []; } const [head, ...tail] = headers; const section = head.parentElement; const id = head.id || section.id; const level = Number(head.tagName.slice(1)); let i = tail.findIndex((s) => Number(s.tagName.slice(1)) <= level); i = i === -1 ? tail.length : i; return [ { headingHtml: htmlPurify.sanitize(head.innerHTML), headingText: head.textContent?.trim().replace(/\s+/g, " ") || "", level, ...href && id && { href: `${href}#${encodeURIComponent(id)}` }, ...id && { id }, children: traverse(tail.slice(0, i)) }, ...traverse(tail.slice(i)) ]; } return traverse(allHeadings); } var getTocHtmlStyle = ({ pageBreakBefore, pageCounterReset }) => { if (!pageBreakBefore && typeof pageCounterReset !== "number") { return null; } return ( /* css */ ` ${pageBreakBefore ? ( /* css */ `:root { break-before: ${pageBreakBefore}; }` ) : ""} ${// Note: `--vs-document-first-page-counter-reset` is reserved variable name in Vivliostyle base themes typeof pageCounterReset === "number" ? ( /* css */ `@page :nth(1) { --vs-document-first-page-counter-reset: page ${Math.floor(pageCounterReset - 1)}; counter-reset: var(--vs-document-first-page-counter-reset); }` ) : ""} ` ); }; var defaultTocTransform = { transformDocumentList: (nodeList) => (propsList) => { return /* @__PURE__ */ jsx("ol", { children: nodeList.map((a, i) => [a, propsList[i]]).flatMap( ([{ href, title, sections }, { children, ...otherProps }]) => { if (sections?.length === 1 && sections[0].level === 1) { return [children].flat().flatMap((e) => { if (e.type === "element" && e.tagName === "ol") { return e.children; } return e; }); } return /* @__PURE__ */ jsxs("li", { ...otherProps, children: [ /* @__PURE__ */ jsx("a", { ...{ href }, children: title }), children ] }); } ) }); }, transformSectionList: (nodeList) => (propsList) => { return /* @__PURE__ */ jsx("ol", { children: nodeList.map((a, i) => [a, propsList[i]]).map( ([{ headingHtml, href, level }, { children, ...otherProps }]) => { const headingContent = { type: "raw", value: headingHtml }; return /* @__PURE__ */ jsxs("li", { ...otherProps, "data-section-level": level, children: [ href ? /* @__PURE__ */ jsx("a", { ...{ href }, children: headingContent }) : /* @__PURE__ */ jsx("span", { children: headingContent }), children ] }); } ) }); } }; function generateDefaultTocHtml({ language, title }) { const toc = /* @__PURE__ */ jsxs("html", { lang: language, children: [ /* @__PURE__ */ jsxs("head", { children: [ /* @__PURE__ */ jsx("meta", { charset: "utf-8" }), /* @__PURE__ */ jsx("title", { children: title || "" }), /* @__PURE__ */ jsx("style", { "data-vv-style": true }) ] }), /* @__PURE__ */ jsxs("body", { children: [ /* @__PURE__ */ jsx("h1", { children: title || "" }), /* @__PURE__ */ jsx("nav", { id: "toc", role: "doc-toc" }) ] }) ] }); return toHtml(toc); } async function generateTocListSection({ entries, distDir, sectionDepth, transform = {} }) { const { transformDocumentList = defaultTocTransform.transformDocumentList, transformSectionList = defaultTocTransform.transformSectionList } = transform; const structure = await Promise.all( entries.map(async (entry) => { const href = encodeURI(upath4.relative(distDir, entry.target)); const sections = sectionDepth >= 1 ? await getStructuredSectionFromHtml(entry.target, href) : []; return { title: entry.title || upath4.basename(entry.target, ".html"), href: encodeURI(upath4.relative(distDir, entry.target)), sections, children: [] // TODO }; }) ); const docToc = transformDocumentList(structure)( structure.map((doc) => { function renderSectionList(sections) { const nodeList = sections.flatMap((section) => { if (section.level > sectionDepth) { return []; } return section; }); if (nodeList.length === 0) { return []; } return transformSectionList(nodeList)( nodeList.map((node) => ({ children: [renderSectionList(node.children || [])].flat() })) ); } return { children: [renderSectionList(doc.sections || [])].flat() }; }) ); return toHtml(docToc, { allowDangerousHtml: true }); } async function processTocHtml(dom, { manifestPath, tocTitle,