@vivliostyle/cli
Version:
Save the pdf file via headless browser and Vivliostyle.
1,689 lines (1,676 loc) • 120 kB
JavaScript
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,