Skip to content

Commit

Permalink
feat: Add feature generating ToC/cover documents from existing MD/HTML
Browse files Browse the repository at this point in the history
  • Loading branch information
spring-raining committed May 29, 2024
1 parent 60cd15f commit 0515768
Show file tree
Hide file tree
Showing 11 changed files with 422 additions and 144 deletions.
4 changes: 4 additions & 0 deletions schemas/vivliostyle/vivliostyleConfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"type": "object",
"properties": {
"path": { "type": "string" },
"output": { "type": "string" },
"title": { "type": "string" },
"theme": { "$ref": "#/definitions/themeSpecifier" },
"encodingFormat": { "type": "string" },
Expand All @@ -32,6 +33,8 @@
"type": "string",
"const": "contents"
},
"path": { "type": "string" },
"output": { "type": "string" },
"title": { "type": "string" },
"theme": { "$ref": "#/definitions/themeSpecifier" },
"pageBreakBefore": {
Expand All @@ -56,6 +59,7 @@
"const": "cover"
},
"path": { "type": "string" },
"output": { "type": "string" },
"title": { "type": "string" },
"theme": { "$ref": "#/definitions/themeSpecifier" },
"imageSrc": { "type": "string" },
Expand Down
155 changes: 119 additions & 36 deletions src/input/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
log,
logWarn,
openEpubToTmpDirectory,
pathEquals,
readJSON,
statFileSync,
touchTmpFile,
Expand Down Expand Up @@ -94,6 +95,10 @@ export interface ContentsEntry {
title: string;
themes: ParsedTheme[];
source?: undefined;
template?: {
source: string;
type: ManuscriptMediaType;
};
target: string;
sectionDepth: number;
transform: {
Expand All @@ -117,6 +122,10 @@ export interface CoverEntry {
title?: string;
themes: ParsedTheme[];
source?: undefined;
template?: {
source: string;
type: ManuscriptMediaType;
};
target: string;
coverImageSrc: string;
coverImageAlt: string;
Expand Down Expand Up @@ -981,6 +990,8 @@ async function composeProjectConfig<T extends CliFlags>(
if (pkgJson) {
debug('located package.json path', pkgJsonPath);
}
const exportAliases: { source: string; target: string }[] = [];
const tmpPrefix = `.vs-${Date.now()}.`;

const tocConfig = (() => {
const c =
Expand Down Expand Up @@ -1016,10 +1027,68 @@ async function composeProjectConfig<T extends CliFlags>(
cliFlags.author ?? config?.author ?? pkgJson?.author;

const isContentsEntry = (entry: EntryObject): entry is ContentsEntryObject =>
entry.rel === 'contents' && !('path' in entry);
entry.rel === 'contents';
const isCoverEntry = (entry: EntryObject): entry is CoverEntryObject =>
entry.rel === 'cover';
function parseEntry(entry: EntryObject): ParsedEntry {
async function parseEntry(entry: EntryObject): Promise<ParsedEntry> {
let inputInfo:
| (ReturnType<typeof parseFileMetadata> & {
source: string;
// target: string;
type: ManuscriptMediaType;
})
| undefined;

if (entry.path) {
const source = upath.resolve(entryContextDir, entry.path);
let sourceExists = true;
if (!isUrlString(source)) {
// Check file exists
try {
statFileSync(source);
} catch (error) {
if (entry.rel === 'contents' || entry.rel === 'cover') {
// For backward compatibility, we allow missing files then assume that option as `output` field.
logWarn(
chalk.yellowBright(
`The "path" option is set but the file does not exist: ${source}\nMaybe you want to set the "output" field instead.`,
),
);
entry.output = entry.path;
sourceExists = false;
} else {
throw error;
}
}
}
if (sourceExists) {
const type = detectManuscriptMediaType(source);
inputInfo = {
...parseFileMetadata({
type,
sourcePath: source,
workspaceDir,
themesDir,
}),
source,
type,
};
}
}

let target = entry.output
? upath.resolve(workspaceDir, entry.output)
: inputInfo &&
(() => {
const contextEntryPath = upath.relative(
entryContextDir,
inputInfo.source,
);
return upath
.resolve(workspaceDir, contextEntryPath)
.replace(/\.md$/, '.html');
})();

if (isContentsEntry(entry)) {
const themes = entry.theme
? [entry.theme].flat().map((theme) =>
Expand All @@ -1030,18 +1099,33 @@ async function composeProjectConfig<T extends CliFlags>(
themesDir,
}),
)
: [...rootThemes];
: inputInfo?.themes ?? [...rootThemes];
themes.forEach((t) => themeIndexes.add(t));
target ??= tocConfig.target;
if (inputInfo?.source && pathEquals(inputInfo.source, target)) {
const tmpPath = upath.resolve(
upath.dirname(target),
`${tmpPrefix}${upath.basename(target)}`,
);
exportAliases.push({ source: tmpPath, target });
await touchTmpFile(tmpPath);
target = tmpPath;
}
const parsedEntry: ContentsEntry = {
rel: 'contents',
...tocConfig,
title: entry.title ?? tocConfig.title,
target,
title: entry.title ?? inputInfo?.title ?? tocConfig.title,
themes,
pageBreakBefore: entry.pageBreakBefore,
pageCounterReset: entry.pageCounterReset,
...(inputInfo && {
template: { source: inputInfo.source, type: inputInfo.type },
}),
};
return parsedEntry;
}

if (isCoverEntry(entry)) {
const themes = entry.theme
? [entry.theme].flat().map((theme) =>
Expand All @@ -1052,70 +1136,69 @@ async function composeProjectConfig<T extends CliFlags>(
themesDir,
}),
)
: []; // Don't inherit rootThemes for cover documents
: inputInfo?.themes ?? []; // Don't inherit rootThemes for cover documents
themes.forEach((t) => themeIndexes.add(t));
const coverImageSrc = ensureCoverImage(entry.imageSrc || cover?.src);
if (!coverImageSrc) {
throw new Error(
`A CoverEntryObject is set in the entry list but a location of cover file is not set. Please set 'cover' property in your config file.`,
);
}
target ??= upath.resolve(
workspaceDir,
entry.path || cover?.htmlPath || COVER_HTML_FILENAME,
);
if (inputInfo?.source && pathEquals(inputInfo.source, target)) {
const tmpPath = upath.resolve(
upath.dirname(target),
`${tmpPrefix}${upath.basename(target)}`,
);
exportAliases.push({ source: tmpPath, target });
await touchTmpFile(tmpPath);
target = tmpPath;
}
const parsedEntry: CoverEntry = {
rel: 'cover',
target: upath.resolve(
workspaceDir,
entry.path || cover?.htmlPath || COVER_HTML_FILENAME,
),
title: entry.title ?? projectTitle,
target,
title: entry.title ?? inputInfo?.title ?? projectTitle,
themes,
coverImageSrc,
coverImageAlt: entry.imageAlt || cover?.name || COVER_HTML_IMAGE_ALT,
pageBreakBefore: entry.pageBreakBefore,
...(inputInfo && {
template: { source: inputInfo.source, type: inputInfo.type },
}),
};
return parsedEntry;
}
const sourcePath = upath.resolve(entryContextDir, entry.path); // abs
const contextEntryPath = upath.relative(entryContextDir, sourcePath); // rel
const targetPath = upath
.resolve(workspaceDir, contextEntryPath)
.replace(/\.md$/, '.html');
if (!isUrlString(sourcePath)) {
// Check file exists
statFileSync(sourcePath);
}
const type = detectManuscriptMediaType(sourcePath);
const metadata = parseFileMetadata({
type,
sourcePath,
workspaceDir,
themesDir,
});

const title = entry.title ?? metadata.title ?? projectTitle;
// can assume that inputInfo and outputPath is not undefined
inputInfo = inputInfo!;
target = target!;
const themes = entry.theme
? [entry.theme]
.flat()
.map((theme) =>
parseTheme({ theme, context, workspaceDir, themesDir }),
)
: metadata.themes ?? [...rootThemes];
: inputInfo.themes ?? [...rootThemes];
themes.forEach((t) => themeIndexes.add(t));

const parsedEntry: ManuscriptEntry = {
type,
source: sourcePath,
target: targetPath,
title,
type: inputInfo.type,
source: inputInfo.source,
target,
title: entry.title ?? inputInfo.title ?? projectTitle,
themes,
...(entry.rel && { rel: entry.rel }),
};
return parsedEntry;
}

const entries: ParsedEntry[] = config?.entry
? (Array.isArray(config.entry) ? config.entry : [config.entry])
.map(normalizeEntry)
.map(parseEntry)
? await Promise.all(
[config.entry].flat().map(normalizeEntry).map(parseEntry),
)
: [];
if (!entries.length) {
throw new Error(
Expand Down Expand Up @@ -1170,7 +1253,7 @@ async function composeProjectConfig<T extends CliFlags>(
format: 'pub-manifest',
entry: upath.join(workspaceDir, MANIFEST_FILENAME),
},
exportAliases: [],
exportAliases,
manifestPath: upath.join(workspaceDir, MANIFEST_FILENAME),
title: projectTitle || fallbackProjectTitle,
author: projectAuthor,
Expand Down
4 changes: 4 additions & 0 deletions src/output/webbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,10 @@ export async function copyWebPublicationAssets({
},
)),
]);
// Exclude files that will overwrite alias targets
for (const alias of relExportAliases) {
allFiles.delete(alias.target);
}

debug(
'webbook files',
Expand Down
Loading

0 comments on commit 0515768

Please sign in to comment.