@vivliostyle/cli
Version:
Save the pdf file via headless browser and Vivliostyle.
1,605 lines (1,593 loc) • 130 kB
JavaScript
import {
importNodeModule
} from "./chunk-ECEGM36O.js";
import {
DetailError,
GlobMatcher,
Logger,
assertPubManifestSchema,
cwd,
debounce,
detectBrowserPlatform,
getAssetMatcher,
getCacheDir,
getDefaultBrowserTag,
getDefaultEpubOpfPath,
getEpubRootDir,
getFormattedError,
getOsLocale,
getWebPubResourceMatcher,
isInContainer,
isRunningOnWSL,
isValidUri,
openEpub,
parseJsonc,
pathContains,
pathEquals,
prettifySchemaError,
readJSON,
registerExitHandler,
runExitHandlers,
setupConfigFromFlags,
statFileSync,
touchTmpFile,
useTmpDirectory,
writeFileIfChanged
} from "./chunk-DBK27BAR.js";
import {
VivliostyleConfigSchema
} from "./chunk-7GIJVX4M.js";
import {
CONTAINER_LOCAL_HOSTNAME,
CONTAINER_URL,
COVER_HTML_FILENAME,
COVER_HTML_IMAGE_ALT,
DEFAULT_BROWSER_VERSIONS,
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-OAFXM4ES.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", ".ts", ".mts", ".cts", ".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."
);
}
if (config.tasks.some((task) => task.pressReady !== void 0)) {
Logger.logWarn(
`'pressReady' property of Vivliostyle config was deprecated and will be removed in a future release. Please use 'pdfPostprocess.preflight: "press-ready"' property instead.`
);
}
if (config.tasks.some(
(task) => task.output && [task.output].flat().some((o) => o.preflight)
)) {
Logger.logWarn(
"'preflight' property of output config was deprecated and will be removed in a future release. Please use 'pdfPostprocess.preflight' property instead."
);
}
if (config.tasks.some(
(task) => task.output && [task.output].flat().some((o) => o.preflightOption)
)) {
Logger.logWarn(
"'preflightOption' property of output config was deprecated and will be removed in a future release. Please use 'pdfPostprocess.preflightOption' property instead."
);
}
}
// 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: vite5,
viteConfigFile,
host,
port,
...overrideInlineOptions
} = inlineConfig;
return {
tasks: tasks.map((task) => ({
...pruneObject(task),
...pruneObject({
theme: theme === false ? void 0 : theme,
size,
pressReady,
title,
author,
language,
readingProgression,
timeout,
image,
viewer,
viewerParam,
browser,
vite: vite5,
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({
renderMode,
preflight,
preflightOption
}),
...pruneObject(
overrideInlineOptions
)
}
};
}
// src/config/resolve.ts
import {
readMetadata,
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 { globSync } from "tinyglobby";
import upath2 from "upath";
// src/processor/markdown.ts
import fs2 from "node:fs";
import vfile from "vfile";
async function processMarkdown(documentProcessorFactory, documentMetadataReader, filepath, options = {}) {
const markdownString = fs2.readFileSync(filepath, "utf8");
const processor = documentProcessorFactory(
options,
documentMetadataReader(markdownString)
);
const processed = await processor.process(
vfile({ path: filepath, contents: markdownString })
);
return processed;
}
function readMarkdownMetadata(filepath, documentMetadataReader) {
return documentMetadataReader(fs2.readFileSync(filepath, "utf8"));
}
// src/config/resolve.ts
var manuscriptMediaTypes = [
"text/markdown",
"text/html",
"text/plain",
"application/xhtml+xml",
// a special MIME type indicates that a custom processor is used
"text/x-vivliostyle-custom"
];
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));
}
var htmlExtensions = [".html", ".htm", ".xhtml", ".xht"];
function toHtmlExtension(filename) {
const ext = upath2.extname(filename).toLowerCase();
if (htmlExtensions.includes(
// @ts-expect-error check membership
ext
)) {
return filename;
}
return `${filename.slice(0, -ext.length)}.html`;
}
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,
documentMetadataReader
}) {
const sourceDir = upath2.dirname(sourcePath);
let title;
let themes;
if (documentMetadataReader) {
const metadata = readMarkdownMetadata(sourcePath, documentMetadataReader);
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 if (contentType === "text/html" || contentType === "application/xhtml+xml") {
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 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 = (() => {
const type = config.browser?.type ?? "chrome";
const platform = detectBrowserPlatform();
return {
type,
tag: config.browser?.tag ?? (platform ? DEFAULT_BROWSER_VERSIONS[type][platform] : "latest"),
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 resolveCmykConfig = (cmykOption) => {
if (cmykOption && typeof cmykOption === "object") {
return {
warnUnmapped: cmykOption.warnUnmapped ?? true,
overrideMap: cmykOption.overrideMap ?? [],
mapOutput: cmykOption.mapOutput ? upath2.resolve(context, cmykOption.mapOutput) : void 0
};
}
if (options.cmyk || cmykOption === true) {
return { warnUnmapped: true, overrideMap: [], mapOutput: void 0 };
}
return false;
};
const resolveReplaceImageConfig = (replaceImageOption) => {
if (!replaceImageOption) {
return [];
}
const allFiles = globSync("**/*", {
cwd: entryContextDir,
onlyFiles: true
});
return replaceImageOption.flatMap(({ source, replacement }) => {
if (source instanceof RegExp) {
return allFiles.filter((file) => source.test(file)).map((file) => ({
source: upath2.resolve(entryContextDir, file),
replacement: upath2.resolve(
entryContextDir,
file.replace(source, replacement)
)
}));
}
return {
source: upath2.resolve(entryContextDir, source),
replacement: upath2.resolve(entryContextDir, replacement)
};
});
};
const resolveDefaultPreflight = () => {
if (options.preflight) {
return options.preflight;
}
const pp = config.pdfPostprocess;
if (pp && "preflight" in pp) {
return pp.preflight;
}
if (config.pressReady) {
return "press-ready";
}
return void 0;
};
const resolveDefaultPreflightOption = () => {
if (options.preflightOption) {
return options.preflightOption;
}
return config.pdfPostprocess?.preflightOption ?? [];
};
const defaultPdfOptions = {
format: "pdf",
renderMode: options.renderMode ?? "local",
preflight: resolveDefaultPreflight(),
preflightOption: resolveDefaultPreflightOption(),
cmyk: resolveCmykConfig(config.pdfPostprocess?.cmyk),
replaceImage: resolveReplaceImageConfig(
config.pdfPostprocess?.replaceImage
)
};
if (config.output) {
return config.output.map((target) => {
const outputPath = upath2.resolve(context, target.path);
const format = target.format;
switch (format) {
case "pdf": {
const targetPp = target.pdfPostprocess;
const { pdfPostprocess: _, ...targetRest } = target;
const resolvedPreflight = (() => {
if (options.preflight) return options.preflight;
if (targetPp?.preflight) return targetPp.preflight;
if (target.preflight) return target.preflight;
return defaultPdfOptions.preflight;
})();
const resolvedPreflightOption = (() => {
if (options.preflightOption) return options.preflightOption;
if (targetPp?.preflightOption) return targetPp.preflightOption;
if (target.preflightOption) return target.preflightOption;
return defaultPdfOptions.preflightOption;
})();
const resolvedCmyk = targetPp?.cmyk !== void 0 ? resolveCmykConfig(targetPp.cmyk) : defaultPdfOptions.cmyk;
const resolvedReplaceImage = targetPp?.replaceImage !== void 0 ? resolveReplaceImageConfig(targetPp.replaceImage) : defaultPdfOptions.replaceImage;
return {
...defaultPdfOptions,
...targetRest,
format,
path: outputPath,
preflight: resolvedPreflight,
preflightOption: resolvedPreflightOption,
cmyk: resolvedCmyk,
replaceImage: resolvedReplaceImage
};
}
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,
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 documentProcessor = {
processorFactory: config.documentProcessor ?? VFM,
metadataReader: config.documentMetadataReader ?? readMetadata
};
const metadata = parseFileMetadata({
contentType,
sourcePath,
workspaceDir,
documentMetadataReader: documentProcessor.metadataReader
});
const target = toHtmlExtension(
upath2.resolve(
workspaceDir,
`${temporaryFilePrefix}${upath2.basename(sourcePath)}`
)
);
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,
documentProcessor
},
target,
title: metadata.title,
themes
});
exportAliases.push({
source: target,
target: upath2.resolve(
upath2.dirname(target),
toHtmlExtension(upath2.basename(sourcePath))
)
});
}
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 rootDocumentProcessor = {
processorFactory: config.documentProcessor ?? VFM,
metadataReader: config.documentMetadataReader ?? readMetadata
};
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 rawContentType = mime(pathname);
const documentProcessor = {
processorFactory: "documentProcessor" in entry && entry.documentProcessor || rootDocumentProcessor.processorFactory,
metadataReader: "documentMetadataReader" in entry && entry.documentMetadataReader || rootDocumentProcessor.metadataReader
};
const hasCustomProcessor = !!(documentProcessor.processorFactory !== VFM || documentProcessor.metadataReader !== readMetadata);
const contentType = hasCustomProcessor && rawContentType !== "text/markdown" ? "text/x-vivliostyle-custom" : rawContentType;
if (!isManuscriptMediaType(contentType) || contentType === "text/plain") {
throw new Error(
`Invalid manuscript type ${rawContentType} detected: ${entry}`
);
}
const useDocumentProcessor = contentType === "text/markdown" || contentType === "text/x-vivliostyle-custom";
return {
type: "file",
pathname,
contentType,
metadata: parseFileMetadata({
contentType,
sourcePath: pathname,
workspaceDir,
themesDir,
documentMetadataReader: useDocumentProcessor ? documentProcessor.metadataReader : void 0
}),
...useDocumentProcessor && { documentProcessor }
};
};
const getTargetPath = (source) => {
switch (source.type) {
case "file":
return upath2.resolve(
workspaceDir,
toHtmlExtension(upath2.relative(entryContextDir, source.pathname))
);
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/vite/vite-plugin-browser.ts
import "vite";
// src/browser.ts
import fs4 from "node:fs";
import upath3 from "upath";
var browserEnumMap = {
chrome: "chrome",
chromium: "chromium",
firefox: "firefox"
};
async function launchBrowser({
browserType,
proxy,
executablePath,
headless,
noSandbox,
disableDevShmUsage,
ignoreHttpsErrors,
protocolTimeout
}) {
const puppeteer = await importNodeModule("puppeteer-core");
const args = [];
if (browserType === "chrome" || browserType === "chromium") {
args.push(
"--disable-field-trial-config",
"--disable-back-forward-cache",
"--disable-component-update",
"--no-default-browser-check",
"--disable-features=AcceptCHFrame,AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument",
"--enable-features=CDPScreenshotNewSurface",
"--no-service-autorun",
"--unsafely-disable-devtools-self-xss-warnings",
"--edge-skip-compat-layer-relaunch"
);
if (process.platform === "darwin") {
args.push("--enable-unsafe-swiftshader");
}
if (noSandbox) {
args.push("--no-sandbox");
}
if (headless) {
args.push(
"--hide-scrollbars",
"--mute-audio",
"--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4"
);
}
if (proxy?.server) {
const proxyURL = new URL(proxy.server);
const isSocks = proxyURL.protocol === "socks5:";
if (isSocks) {
args.push(
`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`
);
}
args.push(`--proxy-server=${proxy.server}`);
const proxyBypassRules = [];
if (proxy.bypass) {
proxyBypassRules.push(
...proxy.bypass.split(",").map((t) => t.trim()).map((t) => t.startsWith(".") ? "*" + t : t)
);
}
proxyBypassRules.push("<-loopback>");
args.push(`--proxy-bypass-list=${proxyBypassRules.join(";")}`);
}
args.push("--disable-web-security");
if (disableDevShmUsage) {
args.push("--disable-dev-shm-usage");
}
if (headless) {
args.push("--force-device-scale-factor=1");
}
if (isRunningOnWSL()) {
args.push("--disable-gpu");
}
args.push("--lang=en");
if (!headless && process.platform === "darwin") {
args.push("-AppleLanguages", "(en)");
}
args.push("--no-startup-window");
}
const launchOptions = {
executablePath,
args,
browser: browserType === "chromium" ? "chrome" : browserType,
headless,
acceptInsecureCerts: ignoreHttpsErrors,
waitForInitialPage: false,
protocolTimeout
};
Logger.debug("launchOptions %O", launchOptions);
const browser = await puppeteer.launch({
...launchOptions,
env: { ...process.env, LANG: "en.UTF-8" }
});
registerExitHandler("Closing browser", async () => {
await browser.close();
});
const [browserContext] = browser.browserContexts();
return { browser, browserContext };
}
function getPuppeteerCacheDir() {
if (isInContainer()) {
return "/opt/puppeteer";
}
return upath3.join(getCacheDir(), "browsers");
}
async function resolveBuildId({
type,
tag,
browsers
}) {
const cacheDataFilename = upath3.join(
getPuppeteerCacheDir(),
"build-ids.json"
);
let cacheData;
try {
cacheData = JSON.parse(fs4.readFileSync(cacheDataFilename, "utf-8"));
if (Date.now() - cacheData.createdAt > 24 * 60 * 60 * 1e3) {
cacheData = { createdAt: Date.now(), buildIds: {} };
}
} catch (_) {
cacheData = { createdAt: Date.now(), buildIds: {} };
}
if (cacheData.buildIds[type]?.[tag]) {
return cacheData.buildIds[type][tag];
}
const platform = detectBrowserPlatform();
if (!platform) {
throw new Error("The current platform is not supported.");
}
const buildId = await browsers.resolveBuildId(
browserEnumMap[type],
platform,
tag
);
(cacheData.buildIds[type] ??= {})[tag] = buildId;
fs4.mkdirSync(upath3.dirname(cacheDataFilename), { recursive: true });
fs4.writeFileSync(cacheDataFilename, JSON.stringify(cacheData));
return buildId;
}
async function cleanupOutdatedBrowsers() {
for (const browser of Object.values(browserEnumMap)) {
const browsersDir = upath3.join(getPuppeteerCacheDir(), browser);
if (!fs4.existsSync(browsersDir)) {
continue;
}
const entries = fs4.readdirSync(browsersDir);
for (const entry of entries) {
const entryPath = upath3.join(browsersDir, entry);
const stat = fs4.statSync(entryPath);
if (!stat.isDirectory() || Date.now() - stat.mtimeMs > 7 * 24 * 60 * 60 * 1e3) {
Logger.debug(`Removing outdated browser at ${entryPath}`);
await fs4.promises.rm(entryPath, { recursive: true, force: true });
}
}
}
}
async function getExecutableBrowserPath({
type,
tag
}) {
const browsers = await importNodeModule("@puppeteer/browsers");
const buildId = await resolveBuildId({ type, tag, browsers });
return browsers.computeExecutablePath({
cacheDir: getPuppeteerCacheDir(),
browser: browserEnumMap[type],
buildId
});
}
function checkBrowserAvailability(path) {
return fs4.existsSync(path);
}
async function downloadBrowser({
type,
tag
}) {
const browsers = await importNodeModule("@puppeteer/browsers");
const buildId = await resolveBuildId({ type, tag, browsers });
let installedBrowser;
if (isInContainer()) {
const defaultBrowserVersion = getDefaultBrowserTag("chrome");
Logger.logWarn(
`The container you are using already includes a browser (chrome@${defaultBrowserVersion}); however, the specified browser ${type}@${tag} was not found. Downloading the browser inside the container may take a long time. Consider using a container image that includes the required browser version.`
);
}
{
var _stack = [];
try {
const _2 = __using(_stack, Logger.suspendLogging(
"Rendering browser is not installed yet. Downloading now."
));
installedBrowser = await browsers.install({
cacheDir: getPuppeteerCacheDir(),
browser: browserEnumMap[type],
buildId,
downloadProgressCallback: "default"
});
} catch (_) {
var _error = _, _hasError = true;
} finally {
__callDispose(_stack, _error, _hasError);
}
}
return installedBrowser.executablePath;
}
async function launchPreview({
mode,
url,
onBrowserOpen,
onPageOpen,
config: {
browser: browserConfig,
proxy,
sandbox,
ignoreHttpsErrors,
timeout
}
}) {
let executableBrowser = browserConfig.executablePath;
Logger.debug(`Specified browser path: ${executableBrowser}`);
if (executableBrowser) {
if (!checkBrowserAvailability(executableBrowser)) {
throw new Error(
`Cannot find the browser. Please check the executable browser path: ${executableBrowser}`
);
}
} else if (detectBrowserPlatform() === "linux_arm" && (browserConfig.type === "chrome" || browserConfig.type === "chromium")) {
Logger.logInfo(
"The official Chrome/Chromium binaries are not available for ARM64 Linux. Using the system-installed Chromium browser instead."
);
executableBrowser = "/usr/bin/chromium";
} else {
executableBrowser = await getExecutableBrowserPath(browserConfig);
Logger.debug(`Using default browser: ${executableBrowser}`);
if (!checkBrowserAvailability(executableBrowser)) {
await cleanupOutdatedBrowsers();
await downloadBrowser(browserConfig);
}
}
const { browser, browserContext } = await launchBrowser({
browserType: browserConfig.type,
proxy,
executablePath: executableBrowser,
headless: mode === "build",
noSandbox: !sandbox,
disableDevShmUsage: isInContainer(),
ignoreHttpsErrors,
protocolTimeout: timeout
});
await onBrowserOpen?.(browser);
const page = (await browserContext.pages())[0] ?? await browserContext.newPage();
await page.setViewport(
mode === "build" ? (
// This viewport size is 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
);
await onPageOpen?.(page);
page.on("dialog", () => {
});
if (proxy?.username && proxy?.password) {
await page.authenticate({
username: proxy.username,
password: proxy.password
});
}
await page.goto(url);
return { browser, page };
}
// src/server.ts
import fs10 from "node:fs";
import { URL as URL2 } from "node:url";
import upath12 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 upath9 from "upath";
import "vite";
// src/processor/compile.ts
import "@vivliostyle/jsdom";
import { copy as copy3, move } from "fs-extra/esm";
import fs8 from "node:fs";
import upath8 from "upath";
import serializeToXml2 from "w3c-xmlserializer";
import MIMEType2 from "whatwg-mimetype";
// src/output/webbook.ts
import { copy as copy2 } from "fs-extra/esm";
import { lookup as mime3 } from "mime-types";
import fs6 from "node:fs";
import { pathToFileURL as pathToFileURL5 } from "node:url";
import { glob } 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) {