1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-08-10 21:51:42 +02:00

The book completely rebuilt with book-builder v1

This commit is contained in:
Sergey Konstantinov
2025-01-21 23:39:37 +02:00
parent 4db5d4d014
commit 862784ffb1
160 changed files with 2791 additions and 3876 deletions

View File

@@ -1,27 +0,0 @@
import { resolve } from 'path';
import { readdirSync, writeFileSync, statSync } from 'fs';
const examplesDir = resolve('docs', 'examples');
export const buildLanding = (structure, lang, l10n, templates) => {
const examples = readdirSync(examplesDir);
const landingHtml = templates.landing({
structure: structure,
examples: examples.reduce((examples, folder) => {
const fullName = resolve(examplesDir, folder);
if (statSync(fullName).isDirectory()) {
const name = folder.match(/^\d+\. (.+)$/)[1];
examples.push({
name,
path: `examples/${folder}`
});
}
return examples;
}, []),
l10n: l10n[lang],
lang,
templates
});
writeFileSync(resolve('docs', l10n[lang].landingFile), landingHtml);
};

182
scripts/build-landing.ts Normal file
View File

@@ -0,0 +1,182 @@
import { resolve } from 'node:path';
import { readdir, writeFile } from 'node:fs/promises';
import { statSync } from 'node:fs';
import {
Path,
Structure
} from '@twirl/book-builder';
import {
CustomTemplates,
Example,
ExtraStrings,
linker,
shareLink,
toc
} from '../src/templates';
export const buildLanding = async ({
structure,
examplesDir,
lang,
outFile,
strings,
templates
}: LandingParameters) => {
const examples = await readdir(examplesDir);
const landingHtml = await landingTemplate(
{
structure,
strings,
lang,
templates
},
examples.reduce((examples: Example[], folder) => {
const fullName = resolve(examplesDir, folder);
if (statSync(fullName).isDirectory()) {
const name = folder.match(/^\d+\. (.+)$/)![1];
examples.push({
name,
path: `examples/${folder}` as Path
});
}
return examples;
}, [])
);
await writeFile(outFile, landingHtml);
};
export const landingTemplate = async (
{
structure,
strings,
lang,
templates
}: Omit<LandingParameters, 'examplesDir' | 'outFile'>,
examples: Example[]
) => {
const link = linker(strings, lang);
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/png" href="assets/favicon.png" />
<title>
${strings.author}. ${strings.title}
</title>
<meta
name="description"
content="${strings.description}"
/>
<meta property="og:type" content="article" />
<meta
property="og:title"
content="${strings.author}. ${strings.title}"
/>
<meta
property="og:description"
content="${strings.description}"
/>
<meta property="og:image" content="assets/header.png" />
<meta
property="og:url"
content="${strings.links.githubHref}"
/>
<link rel="stylesheet" href="assets/fonts.css"/>
<link rel="stylesheet" href="assets/landing.css"/>
</head>
<body>
<nav>
<img
class="header"
src="assets/header.jpg"
alt="${strings.author}. ${strings.title}"
/><br />
<header>
<h1>${strings.author}<br/><span class="title">${
strings.title
}</span></h1>
${
strings.landing.subTitle
? `<h2>${strings.landing.subTitle}</h2>`
: ''
}
</header>
<br />${strings.landing.subscribeOn} ${strings.landing.updates
.map(
(source) =>
`<a class="${source}" href="${
strings.links[source + 'Href']
}">${strings.links[source + 'Tag'] || ''}</a>`
)
.join(' · ')}
${
strings.landing.follow && strings.landing.follow.length
? `<br/>${strings.landing.followOn} ${strings.landing.follow
.map(
(source) =>
`<a class="${source}" href="${
strings.links[source + 'Href']
}">${strings.links[source + 'Tag'] || ''}</a>`
)
.join(' · ')}`
: ''
}
<br />${strings.landing.supportThisWork} ${strings.landing.support
.map(
(source) =>
`<a class="${source}" href="${
strings.links[source + 'Href']
}">${strings.links[source + 'Tag'] || ''}</a>`
)
.join(' · ')}
<br />${strings.sidePanel.shareTo}: ${strings.sidePanel.services
.map(
({ key, link }) =>
`<a class="share share-${key}" href="${shareLink(
link,
strings.sidePanel.shareParameters
)}" target="_blank"></a>`
)
.join(' · ')}<br/>⚙️⚙️⚙️
</nav>
${strings.landing.content.join('\n')}
${
strings.landing.download
? `<p>${strings.landing.download} <a href="${link(
undefined,
'pdf'
)}">PDF</a> / <a href="${link(undefined, 'epub')}">EPUB</a> ${
strings.landing.or
} <a href="${link()}">${strings.landing.readOnline}</a>.
</p>`
: `<p>${strings.landing.readOnline}.</p>`
}
<h3>${strings.toc}</h3>
${await toc({ structure, strings, templates, lang }, examples)}
<p>${strings.landing.license}</p>
<p>${strings.sourceCodeAt} <a href="${strings.links.githubHref}">${
strings.links.githubString
}</a></p>
<h3><a name="about-author">${strings.aboutMe.title}</a></h3>
<section class="about-me">
<aside><img src="https://konstantinov.cc/static/me.png"/><br/>${
strings.aboutMe.imageCredit
}</aside>
<div class="content">
${strings.aboutMe.content.join('\n')}</div>
</section>
${strings.landing.footer.join('\n')}
</body>
</html>`;
};
export interface LandingParameters {
structure: Structure;
examplesDir: Path;
lang: string;
outFile: Path;
strings: ExtraStrings;
templates: CustomTemplates;
}

View File

@@ -1,163 +0,0 @@
import { readFileSync, readdirSync, unlinkSync } from 'fs';
import { resolve as pathResolve } from 'path';
import { init, plugins } from '@twirl/book-builder';
import { templates } from '../src/templates.mjs';
import { apiHighlight } from '../src/api-highlight.mjs';
import { buildLanding } from './build-landing.mjs';
const { flags, args } = process.argv.slice(2).reduce(
({ flags, args }, v) => {
switch (v) {
case '--no-cache':
flags.noCache = true;
break;
case '--sample':
flags.sample = true;
}
if (!v.startsWith('--')) {
args.push(v);
}
return { flags, args };
},
{
args: [],
flags: {}
}
);
const l10n = {
en: JSON.parse(readFileSync('./src/en/l10n.json', 'utf-8')),
ru: JSON.parse(readFileSync('./src/ru/l10n.json', 'utf-8'))
};
const css = ['fonts', 'common', 'screen', 'print', 'page', 'epub'].reduce(
(css, file) => {
css[file] = readFileSync(`src/css/${file}.css`).toString('utf-8');
return css;
},
{}
);
const targetCss = {
html: [css.fonts, css.common, css.screen, css.print],
pdf: [css.fonts, css.common, css.print],
epub: [css.common, css.epub]
};
const extraCss = {
html: css.page,
pdf: css.page
};
const langsToBuild = (args[0] && args[0].split(',').map((s) => s.trim())) || [
'ru',
'en'
];
const targets = (
(args[1] && args[1].split(',')) || ['html', 'pdf', 'epub', 'landing']
).reduce((targets, arg) => {
targets[arg.trim()] = true;
return targets;
}, {});
const chapters = args[2];
const noCache = flags.noCache;
if (flags.noCache) {
clean();
}
console.log(`Building langs: ${langsToBuild.join(', ')}`);
(async () => {
for (const lang of langsToBuild) {
await init({
lang,
l10n: l10n[lang],
basePath: pathResolve(`src`),
path: pathResolve(`src/${lang}/clean-copy`),
templates,
pipeline: {
css: {
beforeAll: [
plugins.css.backgroundImageDataUri,
plugins.css.fontFaceDataUri
]
},
ast: {
preProcess: [
plugins.ast.h3ToTitle,
plugins.ast.h5Counter,
plugins.ast.aImg,
plugins.ast.imgSrcResolve,
plugins.ast.highlighter({
languages: ['javascript', 'typescript', 'json'],
languageDefinitions: {
json: apiHighlight
}
}),
plugins.ast.ref,
plugins.ast.ghTableFix,
plugins.ast.stat
]
},
htmlSourceValidator: {
validator: 'WHATWG',
ignore: [
'heading-level',
'no-raw-characters',
'wcag/h37',
'no-missing-references'
]
},
html: {
postProcess: [plugins.html.imgDataUri]
}
},
chapters,
noCache,
sample: flags.sample,
cover: 'src/cover_embed.png'
}).then(async (builder) => {
for (const target of Object.keys(targets)) {
if (target !== 'landing') {
await builder.build(
target,
pathResolve(
'docs',
`${l10n[lang].file}.${lang}${
flags.sample ? '.sample' : ''
}.${target}`
),
{
css: targetCss[target],
extraCss: extraCss[target]
}
);
console.log(
`Finished lang=${lang} target=${target}\n${Object.entries(
{
sources: 'Sources',
references: 'references',
words: 'words',
characters: 'characters'
}
)
.map(([k, v]) => `${v}: ${builder.structure[k]}`)
.join(', ')}`
);
} else {
buildLanding(builder.structure, lang, l10n, templates);
}
}
});
}
})();
function clean() {
const tmpDir = pathResolve('.', '.tmp');
const files = readdirSync(tmpDir);
for (const fileName of files) {
const file = pathResolve(tmpDir, fileName);
unlinkSync(file);
}
}

327
scripts/build.ts Normal file
View File

@@ -0,0 +1,327 @@
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import {
Bibliography,
init,
Path,
plugins,
LogLevel,
applyAstPluginToStructure,
L10n,
} from '@twirl/book-builder';
import { buildLanding } from './build-landing';
import { CustomTemplates, ExtraStrings } from '../src/templates';
const SRC = resolve('./src') as Path;
const LOCALES: { [language: string]: string } = {
en: 'en-US',
ru: 'ru-RU'
};
const { args, flags } = process.argv.slice(2).reduce(
({ args, flags }, v) => {
if (v.startsWith('--')) {
flags.add(
v.slice(2).replace(/-\w/g, (m) => m.charAt(1).toUpperCase())
);
} else {
args.push(v);
}
return { args, flags };
},
{ args: [] as string[], flags: new Set<string>() }
);
async function initBuilder(
language: string,
locale: string,
strings: ExtraStrings,
target: 'epub' | 'html' | 'pdf'
): Promise<{
bookBuilder: Awaited<ReturnType<typeof init>>;
templates: CustomTemplates;
l10n: L10n<CustomTemplates, ExtraStrings>;
}> {
const bookBuilder = await init({
source: {
dir: resolve(SRC, language, 'clean-copy') as Path,
base: SRC
},
options: {
tmpDir: resolve('./.tmp') as Path,
noCache: flags.has('noCache'),
purgeCache: flags.has('purgeCache'),
logLevel: LogLevel.DEBUG
}
});
const templates = new CustomTemplates(
target,
bookBuilder.context,
language,
locale,
{
...strings,
favicon: await bookBuilder.toDataUri(strings.favicon)
},
{ anchorLink: 'anchor', externalLink: 'external' }
);
return {
bookBuilder,
templates,
l10n: { language, locale, strings, templates }
};
}
const builders = {
html: async ({
outFile,
language,
locale,
strings,
bibliography,
cssFiles
}) => {
const { bookBuilder, templates, l10n } = await initBuilder(
language,
locale,
strings,
'html'
);
await bookBuilder.build<CustomTemplates, ExtraStrings, 'html'>(
'html',
l10n,
{
structure: {
plugins: [
plugins.structure.h3Title(),
plugins.structure.h5Counter(),
plugins.structure.highlighter({
languages: ['javascript', 'typescript', 'json']
}),
plugins.structure.hoistSingleChapters(),
plugins.structure.tableOfContents(),
plugins.structure.imprintPages(
templates.htmlImprintPages(),
'front-page'
),
plugins.structure.reference({
bibliography
}),
plugins.structure.imgDataUri(),
plugins.structure.aImg()
]
},
html: {
plugins: [plugins.html.validator()]
},
css: {
plugins: [plugins.css.dataUri()]
}
},
{
css: [cssFiles.FONTS, cssFiles.COMMON, cssFiles.SCREEN].join(
'\n'
),
outFile
}
);
},
epub: async ({
outFile,
language,
locale,
strings,
bibliography,
cssFiles,
baseDir
}) => {
const { bookBuilder, templates, l10n } = await initBuilder(
language,
locale,
strings,
'epub'
);
await bookBuilder.build<CustomTemplates, ExtraStrings, 'epub'>(
'epub',
l10n,
{
structure: {
plugins: [
plugins.structure.h3Title(),
plugins.structure.h5Counter(),
plugins.structure.highlighter({
languages: ['javascript', 'typescript', 'json']
}),
plugins.structure.hoistSingleChapters(),
plugins.structure.imprintPages(
templates.htmlImprintPages(),
'front-page'
),
plugins.structure.reference({
bibliography,
prependPath: 'bibliography.xhtml'
}),
plugins.structure.epubLink(),
plugins.structure.aImg(),
plugins.structure.imgSrcToFileUrl(baseDir)
]
},
epub: {
plugins: []
},
css: {
plugins: []
}
},
{
css: [cssFiles.COMMON, cssFiles.EPUB].join('\n'),
outFile
}
);
},
pdf: async ({
outFile,
language,
locale,
strings,
bibliography,
cssFiles
}) => {
const { bookBuilder, templates, l10n } = await initBuilder(
language,
locale,
strings,
'pdf'
);
await bookBuilder.build<CustomTemplates, ExtraStrings, 'pdf'>(
'pdf',
l10n,
{
structure: {
plugins: [
plugins.structure.h3Title(),
plugins.structure.h5Counter(),
plugins.structure.highlighter({
languages: ['javascript', 'typescript', 'json']
}),
plugins.structure.hoistSingleChapters(),
plugins.structure.tableOfContents(),
plugins.structure.imprintPages(
templates.htmlImprintPages(),
'front-page'
),
plugins.structure.reference({
bibliography
}),
plugins.structure.imgDataUri(),
plugins.structure.aImg()
]
},
pdf: {
plugins: []
},
css: {
plugins: [plugins.css.dataUri()]
}
},
{
css: [
cssFiles.FONTS,
cssFiles.COMMON,
cssFiles.PAGE,
cssFiles.PRINT
].join('\n'),
outFile,
useCachedContent: false
}
);
},
landing: async ({ language, locale, strings }) => {
const { bookBuilder, templates } = await initBuilder(
language,
locale,
strings,
'html'
);
await applyAstPluginToStructure(
bookBuilder.context,
{ language, locale, strings, templates },
bookBuilder.structure,
plugins.structureAst.h3Title()
);
await buildLanding({
structure: bookBuilder.structure,
lang: language,
examplesDir: resolve('docs', 'examples') as Path,
outFile: resolve('docs', strings.landingFile) as Path,
strings,
templates
});
}
} as {
[target: string]: (params: {
outFile: Path;
language: string;
locale: string;
strings: ExtraStrings;
bibliography: Bibliography;
cssFiles: Record<string, string>;
baseDir: Path;
}) => Promise<void>;
};
async function main() {
const cssFiles = {
COMMON: await readFile(resolve(SRC, 'css', 'common.css'), 'utf-8'),
FONTS: await readFile(resolve(SRC, 'css', 'fonts.css'), 'utf-8'),
SCREEN: await readFile(resolve(SRC, 'css', 'screen.css'), 'utf-8'),
EPUB: await readFile(resolve(SRC, 'css', 'epub.css'), 'utf-8'),
PRINT: await readFile(resolve(SRC, 'css', 'print.css'), 'utf-8'),
PAGE: await readFile(resolve(SRC, 'css', 'page.css'), 'utf-8')
};
const languages = (args[0] ?? 'en,ru').split(',').map((s) => s.trim());
const targets = (args[1] ?? 'html,epub,pdf')
.split(',')
.map((s) => s.trim());
for (const language of languages) {
const locale = LOCALES[language];
if (!locale) {
throw new Error(`Unknown locale ${locale}`);
}
const strings = JSON.parse(
await readFile(resolve(SRC, language, 'l10n.json'), 'utf-8')
) as ExtraStrings;
const bibliography = JSON.parse(
await readFile(resolve(SRC, language, 'bibliography.json'), 'utf-8')
) as Bibliography;
for (const target of targets) {
const builder = builders[target];
if (typeof builder !== 'function') {
throw new Error(`Unknown target ${target}`);
}
const outFile = resolve(
'docs',
`API.${language}.${target}`
) as Path;
await builder({
outFile,
language,
locale,
strings,
bibliography,
cssFiles,
baseDir: SRC
});
}
}
}
main()
.catch((e) => console.error(e, e.stack))
.then(() => process.exit(0));