mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-05-25 22:08:06 +02:00
Builder updated
This commit is contained in:
parent
a759c4ec26
commit
5b4eaaf292
142
build.js
142
build.js
@ -1,142 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const templates = require('./src/templates');
|
||||
const builders = require('./src/lib/builders');
|
||||
const mdHtml = require('./src/lib/md-html');
|
||||
const htmlProcess = require('./src/lib/html-process');
|
||||
|
||||
const l10n = {
|
||||
en: require('./src/en/l10n.json'),
|
||||
ru: require('./src/ru/l10n.json')
|
||||
};
|
||||
|
||||
const langsToBuild = process.argv[2] &&
|
||||
process.argv[2].split(',').map((s) => s.trim()) ||
|
||||
['ru', 'en'];
|
||||
|
||||
const targets = (process.argv[3] &&
|
||||
process.argv[3].split(',') ||
|
||||
['html', 'pdf', 'epub']
|
||||
).reduce((targets, arg) => {
|
||||
targets[arg.trim()] = true;
|
||||
return targets;
|
||||
}, {});
|
||||
|
||||
buildDocs(langsToBuild, targets, l10n).then(() => {
|
||||
console.log('Done!');
|
||||
process.exit(0);
|
||||
}, (e) => {
|
||||
console.error(e);
|
||||
process.exit(255);
|
||||
});
|
||||
|
||||
function buildDocs (langsToBuild, targets, l10n) {
|
||||
console.log(`Building in following languages: ${
|
||||
langsToBuild.join(', ')
|
||||
}, targets: ${
|
||||
Object.keys(targets).join(', ')
|
||||
}`);
|
||||
|
||||
return Promise.all(
|
||||
langsToBuild.map((lang) => buildDoc(lang, targets, l10n[lang]))
|
||||
);
|
||||
}
|
||||
|
||||
async function buildDoc (lang, targets, l10n) {
|
||||
const pageBreak = templates.pageBreak;
|
||||
const structure = await getStructure({
|
||||
path: `./src/${lang}/clean-copy/`,
|
||||
l10n,
|
||||
pageBreak
|
||||
});
|
||||
const tableOfContents = templates.toc(structure, l10n);
|
||||
const htmlContent = [
|
||||
structure.frontPage,
|
||||
tableOfContents,
|
||||
...structure.sections
|
||||
.map((section) => section.chapters.reduce((content, chapter) => {
|
||||
if (chapter.title) {
|
||||
content.push(templates.chapterTitle(chapter));
|
||||
}
|
||||
content.push(chapter.content);
|
||||
return content;
|
||||
}, [templates.sectionTitle(section)]).join(''))
|
||||
];
|
||||
|
||||
return Promise.all(['html', 'pdf', 'epub'].map((target) => {
|
||||
if (targets[target]) {
|
||||
return prepareHtml(htmlContent.join(''), l10n, target).then((html) => {
|
||||
return builders[target]({
|
||||
lang,
|
||||
structure,
|
||||
html,
|
||||
l10n,
|
||||
path: path.join(__dirname, 'docs', `API.${lang}.${target}`)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async function getStructure ({ path, l10n, pageBreak}) {
|
||||
const structure = {
|
||||
frontPage: fs.readFileSync(`${path}intro.html`, 'utf-8') + pageBreak,
|
||||
sections: []
|
||||
};
|
||||
let counter = 1;
|
||||
|
||||
await fs.readdirSync(path)
|
||||
.filter((p) => fs.statSync(`${path}${p}`).isDirectory())
|
||||
.sort()
|
||||
.reduce(async (p, dir, index) => {
|
||||
const structure = await p;
|
||||
const name = dir.split('-')[1];
|
||||
const section = {
|
||||
title: name,
|
||||
anchor: `section-${index + 1}`,
|
||||
chapters: []
|
||||
}
|
||||
|
||||
const subdir = `${path}${dir}/`;
|
||||
await fs.readdirSync(subdir)
|
||||
.filter((p) => fs.statSync(`${subdir}${p}`).isFile() && p.indexOf('.md') == p.length - 3)
|
||||
.sort()
|
||||
.reduce(async (p, file) => {
|
||||
const section = await p;
|
||||
const md = fs.readFileSync(`${subdir}${file}`, 'utf-8').trim();
|
||||
const content = await mdHtml(md, {
|
||||
counter,
|
||||
l10n,
|
||||
base: __dirname
|
||||
});
|
||||
section.chapters.push({
|
||||
anchor: content.data.anchor,
|
||||
title: content.data.title,
|
||||
content: content.contents + pageBreak
|
||||
});
|
||||
counter++;
|
||||
return section;
|
||||
}, Promise.resolve(section));
|
||||
|
||||
structure.sections.push(section);
|
||||
return structure;
|
||||
}, Promise.resolve(structure));
|
||||
|
||||
|
||||
return structure;
|
||||
}
|
||||
|
||||
async function prepareHtml (content, l10n, target) {
|
||||
if (target == 'epub') {
|
||||
return '';
|
||||
} else {
|
||||
return (await htmlProcess(
|
||||
templates[target == 'html' ? 'screenHtml' : 'printHtml'](content, l10n), {
|
||||
base: __dirname
|
||||
}
|
||||
)).contents;
|
||||
}
|
||||
}
|
61
build.mjs
Normal file
61
build.mjs
Normal file
@ -0,0 +1,61 @@
|
||||
import { resolve as pathResolve } from 'path';
|
||||
import templates from './src/templates.js';
|
||||
import { init, plugins } from '../The-Book-Builder/index.js';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
const l10n = {
|
||||
en: JSON.parse(readFileSync('./src/en/l10n.json', 'utf-8')),
|
||||
ru: JSON.parse(readFileSync('./src/ru/l10n.json', 'utf-8'))
|
||||
};
|
||||
|
||||
const langsToBuild = (process.argv[2] &&
|
||||
process.argv[2].split(',').map((s) => s.trim())) || ['ru', 'en'];
|
||||
|
||||
const targets = (
|
||||
(process.argv[3] && process.argv[3].split(',')) || ['html', 'pdf', 'epub']
|
||||
).reduce((targets, arg) => {
|
||||
targets[arg.trim()] = true;
|
||||
return targets;
|
||||
}, {});
|
||||
|
||||
console.log(`Building langs: ${langsToBuild.join(', ')}…`);
|
||||
langsToBuild.forEach((lang) => {
|
||||
init({
|
||||
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.ref,
|
||||
plugins.ast.ghTableFix
|
||||
]
|
||||
},
|
||||
htmlSourceValidator: {
|
||||
validator: 'WHATWG',
|
||||
ignore: ['heading-level', 'no-raw-characters', 'wcag/h37']
|
||||
},
|
||||
html: {
|
||||
postProcess: [plugins.html.imgDataUri]
|
||||
}
|
||||
}
|
||||
}).then((builder) => {
|
||||
Object.keys(targets).forEach((target) => {
|
||||
builder.build(
|
||||
target,
|
||||
pathResolve(`docs/${l10n[lang].file}.${lang}.${target}`)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
BIN
docs/API.en.epub
BIN
docs/API.en.epub
Binary file not shown.
375
docs/API.en.html
375
docs/API.en.html
File diff suppressed because one or more lines are too long
BIN
docs/API.en.pdf
BIN
docs/API.en.pdf
Binary file not shown.
BIN
docs/API.ru.epub
BIN
docs/API.ru.epub
Binary file not shown.
1167
docs/API.ru.html
1167
docs/API.ru.html
File diff suppressed because one or more lines are too long
BIN
docs/API.ru.pdf
BIN
docs/API.ru.pdf
Binary file not shown.
12
package.json
12
package.json
@ -5,17 +5,9 @@
|
||||
"author": "Sergey Konstantinov <twirl-team@yandex.ru>",
|
||||
"repository": "github.com:twirl/The-API-Book",
|
||||
"devDependencies": {
|
||||
"css": "^3.0.0",
|
||||
"epub-gen": "^0.1.0",
|
||||
"image-data-uri": "^2.0.1",
|
||||
"puppeteer": "^5.5.0",
|
||||
"rehype-parse": "^7.0.1",
|
||||
"rehype-stringify": "^8.0.0",
|
||||
"remark-parse": "^8.0.3",
|
||||
"remark-rehype": "^7.0.0",
|
||||
"unified": "^9.2.0"
|
||||
"@twirl/book-builder": "0.0.9"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build.js"
|
||||
"build": "node build.mjs"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14pt;
|
||||
@ -8,11 +9,17 @@ html, body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
pre, img {
|
||||
pre,
|
||||
img {
|
||||
margin-right: 0.1em;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
page-break-after: never;
|
||||
}
|
||||
|
||||
@ -28,7 +35,7 @@ img {
|
||||
}
|
||||
|
||||
.cover {
|
||||
background-image: url(./src/cover_300dpi.png);
|
||||
background-image: url(/cover_300dpi.png);
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 100%;
|
||||
@ -48,7 +55,9 @@ h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p, ul, ol {
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
orphans: 4;
|
||||
}
|
||||
|
||||
@ -60,4 +69,4 @@ p, ul, ol {
|
||||
@page:first {
|
||||
margin: 0;
|
||||
size: 7.5in 10in;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
background-image: url(./src/cover_96dpi.png);
|
||||
background-image: url(/cover_96dpi.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 100%;
|
||||
height: 1056px;
|
||||
@ -9,4 +9,4 @@ h1 {
|
||||
margin: 0 auto;
|
||||
padding-top: 4em;
|
||||
padding-left: 2em;
|
||||
}
|
||||
}
|
@ -4,7 +4,8 @@ html {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body, h6 {
|
||||
body,
|
||||
h6 {
|
||||
font-family: 'PT Serif';
|
||||
font-size: 14pt;
|
||||
text-align: justify;
|
||||
@ -24,7 +25,8 @@ body, h6 {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
code,
|
||||
pre {
|
||||
font-family: Inconsolata, sans-serif;
|
||||
}
|
||||
|
||||
@ -32,17 +34,17 @@ code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
p img {
|
||||
.img-wrapper img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
border-radius: .25em;
|
||||
border-top: 1px solid rgba(0,0,0,.45);
|
||||
border-left: 1px solid rgba(0,0,0,.45);
|
||||
box-shadow: .1em .1em .1em rgba(0,0,0,.45);
|
||||
border-radius: 0.25em;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.45);
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.45);
|
||||
box-shadow: 0.1em 0.1em 0.1em rgba(0, 0, 0, 0.45);
|
||||
page-break-inside: avoid;
|
||||
overflow-x: auto;
|
||||
font-size: 90%;
|
||||
@ -50,7 +52,7 @@ pre {
|
||||
|
||||
img:not(.cc-by-nc-img) {
|
||||
border: 1px solid darkgray;
|
||||
box-shadow: .1em .1em .1em rgba(0,0,0,.45);
|
||||
box-shadow: 0.1em 0.1em 0.1em rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
pre code {
|
||||
@ -69,7 +71,11 @@ a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
text-align: left;
|
||||
font-family: 'PT Sans';
|
||||
font-weight: bold;
|
||||
@ -104,7 +110,8 @@ h3 {
|
||||
font-variant: small-caps;
|
||||
}
|
||||
|
||||
h4, h5 {
|
||||
h4,
|
||||
h5 {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
@ -154,15 +161,17 @@ a.anchor {
|
||||
h5 a.anchor:before {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
|
||||
a.anchor:hover:before {
|
||||
color: black;
|
||||
}
|
||||
|
||||
h2:not(.toc), h3, h5 {
|
||||
h2:not(.toc),
|
||||
h3,
|
||||
h5 {
|
||||
text-indent: -0.8em;
|
||||
}
|
||||
|
||||
|
||||
@keyframes octocat-wave {
|
||||
0%,
|
||||
100% {
|
||||
@ -192,7 +201,8 @@ a.anchor {
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
@ -14,8 +14,7 @@ In other words, hundreds or even thousands of different APIs must work correctly
|
||||
|
||||
When I'm asked of an example of a well-designed API, I usually show the picture of a Roman aqueduct:
|
||||
|
||||

|
||||
###### Photo credit: [igorelick @ pixabay](https://pixabay.com/photos/pont-du-gard-france-aqueduct-bridge-3909998/)
|
||||
[](https://pixabay.com/photos/pont-du-gard-france-aqueduct-bridge-3909998/)
|
||||
|
||||
* it interconnects two areas;
|
||||
* backwards compatibility being broken not a single time in two thousand years.
|
||||
|
@ -225,7 +225,7 @@ GET /v1/orders/{id}
|
||||
{ "order_id", "status" … }
|
||||
```
|
||||
```
|
||||
// Returns all customers's orders
|
||||
// Returns all customer's orders
|
||||
// in all statuses
|
||||
GET /v1/users/{id}/orders
|
||||
```
|
||||
|
@ -39,7 +39,7 @@ Saying nothing about Fielding's loose interpretation of his own dissertation, le
|
||||
|
||||
#### REST: The Good Part
|
||||
|
||||
We don't actually know why of all overviews of abstract network-based architectures the Fielding's one became the most widely known. But it's clearly obvious that Fielding's theory being reflected in minds of millions of developers (including Fielding's own) morphed into a whole engineering subculture. Out of reducing the REST abstractions to the HTTP protocol and the URL standard, the chimaera of ‘RESTful API’ was born — the one which [nobody knowns the exact sense of](https://restfulapi.net/).
|
||||
We don't actually know why of all overviews of abstract network-based architectures the Fielding's one became the most widely known. But it's clearly obvious that Fielding's theory being reflected in minds of millions of developers (including Fielding's own) morphed into a whole engineering subculture. Out of reducing the REST abstractions to the HTTP protocol and the URL standard, the chimaera of ‘RESTful API’ was born — the one which [nobody knows the exact sense of](https://restfulapi.net/).
|
||||
|
||||
Are we saying that REST concept is meaningless? Not at all. We were just trying to demonstrate that it allows for too loose interpretation, which is simultaneously its main power and its main flaw.
|
||||
|
||||
@ -61,17 +61,17 @@ Why we say this is ‘right’? Because modern client-server interaction stack i
|
||||
|
||||
(Actually, with regards to many technical aspects interim agents are taking many opportunities, not asking the developers about them. For example, freely changing `Accept-Encoding` and therefore `Content-Length` while proxying requests and responses.)
|
||||
|
||||
Every REST principle, named by Fielding, allows for making interim software work better. The stateless paradigm is a key: proxies might be sure that request's metadata describe it unambiguosly.
|
||||
Every REST principle, named by Fielding, allows for making interim software work better. The stateless paradigm is a key: proxies might be sure that request's metadata describe it unambiguously.
|
||||
|
||||
Let's explore a simple example. Imagine we have operations for getting and deleting user's profile in our system. We may organize them in different ways. For example, like this:
|
||||
|
||||
```
|
||||
// Get user's profile
|
||||
GET /me
|
||||
Cookie: session_id=<идентификатор сессии>
|
||||
Cookie: session_id=<session identifier>
|
||||
// Delete user's profile
|
||||
GET /delete-me
|
||||
Cookie: session_id=<идентификатор сессии>
|
||||
Cookie: session_id=<session identifier>
|
||||
```
|
||||
|
||||
Why this solution is defective from the interim agent's point of view?
|
||||
|
@ -6,5 +6,7 @@
|
||||
"frontPage": "Front Page",
|
||||
"description": "Designing APIs is a very special skill: API is a multiplier to both your opportunities and mistakes. This book is written to share the expertise and describe the best practices in the API design. The book comprises three large sections. In Section I we'll discuss designing APIs as a concept: how to build the architecture properly, from a high-level planning down to final interfaces. Section II is dedicated to an API's lifecycle: how interfaces evolve over time, and how to elaborate the product to match users' needs. Finally, Section III is more about un-engineering sides of the API, like API marketing, organizing support, and working with a community.",
|
||||
"locale": "en_US",
|
||||
"url": "https://twirl.github.io/The-API-Book/docs/API.en.html"
|
||||
}
|
||||
"file": "API",
|
||||
"url": "https://twirl.github.io/The-API-Book/docs/API.en.html",
|
||||
"imageCredit": "Image Credit"
|
||||
}
|
||||
|
@ -14,8 +14,7 @@
|
||||
|
||||
Когда меня просят привести пример хорошего API, я обычно показываю фотографию древнеримского акведука:
|
||||
|
||||

|
||||
###### Photo credit: [igorelick @ pixabay](https://pixabay.com/photos/pont-du-gard-france-aqueduct-bridge-3909998/)
|
||||
[](https://pixabay.com/photos/pont-du-gard-france-aqueduct-bridge-3909998/)
|
||||
|
||||
* он связывает между собой две области
|
||||
* обратная совместимость нарушена ноль раз за последние две тысячи лет.
|
||||
|
@ -6,5 +6,7 @@
|
||||
"frontPage": "Титульный лист",
|
||||
"description": "Разработка API — особый навык: API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок. Эта книга написана для того, чтобы поделиться опытом и изложить лучшие практики проектирования API. Книга состоит из трёх больших разделов. В первом разделе мы поговорим о проектировании API на стадии разработки концепции — как грамотно выстроить архитектуру, от крупноблочного планирования до конечных интерфейсов. Второй раздел будет посвящён жизненному циклу API — как интерфейсы эволюционируют со временем и как развивать продукт так, чтобы отвечать потребностям пользователей. Наконец, третий раздел будет касаться больше не-разработческих сторон жизни API — поддержки, маркетинга, работы с комьюнити.",
|
||||
"locale": "ru_RU",
|
||||
"url": "https://twirl.github.io/The-API-Book/docs/API.ru.html"
|
||||
}
|
||||
"file": "API",
|
||||
"url": "https://twirl.github.io/The-API-Book/docs/API.ru.html",
|
||||
"imageCredit": "Image Credit"
|
||||
}
|
||||
|
@ -1,79 +1,3 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const css = fs.readFileSync(path.resolve(__dirname, 'style.css'), 'utf-8');
|
||||
|
||||
const cssProcess = require('./lib/css-process');
|
||||
const printCss = cssProcess(fs.readFileSync(path.resolve(__dirname, 'print.css'), 'utf-8'));
|
||||
const screenCss = cssProcess(fs.readFileSync(path.resolve(__dirname, 'screen.css'), 'utf-8'));
|
||||
|
||||
const templates = module.exports = {
|
||||
screenHtml: (html, l10n) => {
|
||||
return `<html><head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>${l10n.author}. ${l10n.title}</title>
|
||||
<meta name="author" content="${l10n.author}"/>
|
||||
<meta name="description" content="${l10n.description}"/>
|
||||
<meta property="og:title" content="${l10n.author}. ${l10n.title}"/>
|
||||
<meta property="og:url" content="${l10n.url}"/>
|
||||
<meta property="og:type" content="article"/>
|
||||
<meta property="og:description" content="${l10n.description}"/>
|
||||
<meta property="og:locale" content="${l10n.locale}"/>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=PT+Serif&family=PT+Sans&family=Inconsolata"/>
|
||||
<style>${css}</style>
|
||||
<style>${screenCss}</style>
|
||||
</head><body>
|
||||
<article>
|
||||
${html}
|
||||
</article>
|
||||
</body></html>`;
|
||||
},
|
||||
|
||||
printHtml: (html, l10n) => {
|
||||
return `<html><head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>${l10n.author}. ${l10n.title}</title>
|
||||
<meta name="author" content="${l10n.author}"/>
|
||||
<meta name="description" content="${l10n.description}"/>
|
||||
<meta property="og:title" content="${l10n.author}. ${l10n.title}"/>
|
||||
<meta property="og:url" content="${l10n.url}"/>
|
||||
<meta property="og:type" content="article"/>
|
||||
<meta property="og:description" content="${l10n.description}"/>
|
||||
<meta property="og:locale" content="${l10n.locale}"/>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=PT+Serif&family=PT+Sans&family=Inconsolata"/>
|
||||
<style>${css}</style>
|
||||
<style>${printCss}</style>
|
||||
</head><body>
|
||||
<article>
|
||||
${html}
|
||||
</article>
|
||||
</body></html>`;
|
||||
},
|
||||
|
||||
toc: (structure, l10n) => {
|
||||
return `<nav><h2 class="toc">${l10n.toc}</h2><ul class="table-of-contents">${
|
||||
structure.sections.map((section) => {
|
||||
return `<li><a href="#${section.anchor}">${section.title}</a><ul>${
|
||||
section.chapters.map((chapter) => {
|
||||
return `<li><a href="#${chapter.anchor}">${chapter.title}</a></li>`
|
||||
}).join('')
|
||||
}</ul></li>`;
|
||||
}).join('')
|
||||
}</ul></nav>${templates.pageBreak}`
|
||||
},
|
||||
|
||||
ref: (anchor, content) => {
|
||||
return `<a href="#${anchor}" class="anchor" name="${anchor}">${content}</a>`;
|
||||
},
|
||||
|
||||
sectionTitle: (section) => {
|
||||
return section.title ?
|
||||
`<h2>${templates.ref(section.anchor, section.title)}</h2>` :
|
||||
'';
|
||||
},
|
||||
|
||||
chapterTitle: (chapter) => {
|
||||
return `<h3>${templates.ref(chapter.anchor, chapter.title)}</h3>`;
|
||||
},
|
||||
|
||||
const templates = (module.exports = {
|
||||
pageBreak: '<div class="page-break"></div>'
|
||||
};
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user