1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-06-30 22:43:38 +02:00

Images support added

This commit is contained in:
Sergey Konstantinov
2020-12-20 14:31:45 +03:00
parent 5c9da3dcc6
commit 2f0002ff99
19 changed files with 784 additions and 403 deletions

View File

@ -1,18 +1,11 @@
const fs = require('fs'); const fs = require('fs');
const mdHtml = new (require('showdown').Converter)();
const path = require('path'); const path = require('path');
const langsToBuild = process.argv[2] && const builders = require('./src/lib/builders');
process.argv[2].split(',').map((s) => s.trim()) || const mdHtml = require('./src/lib/md-html');
['ru', 'en']; const htmlProcess = require('./src/lib/html-process');
const targets = (process.argv[3] && const css = fs.readFileSync('./src/style.css', 'utf-8');
process.argv[3].split(',') ||
['html', 'pdf', 'epub']
).reduce((targets, arg) => {
targets[arg.trim()] = true;
return targets;
}, {});
const l10n = { const l10n = {
en: { en: {
@ -34,9 +27,18 @@ const l10n = {
locale: 'ru_RU' locale: 'ru_RU'
} }
}; };
const css = fs.readFileSync('src/style.css', 'utf-8');
const builders = require('./builders'); 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(() => { buildDocs(langsToBuild, targets, l10n).then(() => {
console.log('Done!'); console.log('Done!');
@ -58,9 +60,9 @@ function buildDocs (langsToBuild, targets, l10n) {
); );
} }
function buildDoc (lang, targets, l10n) { async function buildDoc (lang, targets, l10n) {
const pageBreak = '<div class="page-break"></div>'; const pageBreak = '<div class="page-break"></div>';
const structure = getStructure({ const structure = await getStructure({
path: `./src/${lang}/clean-copy/`, path: `./src/${lang}/clean-copy/`,
l10n, l10n,
pageBreak pageBreak
@ -88,9 +90,9 @@ function buildDoc (lang, targets, l10n) {
content.push(chapter.content); content.push(chapter.content);
return content; return content;
}, [section.title ? `<h2>${getRef(section.anchor)}${section.title}</h2>` : '']).join('')) }, [section.title ? `<h2>${getRef(section.anchor)}${section.title}</h2>` : '']).join(''))
].join('\n'); ];
const html = `<html><head> const html = targets.html || targets.pdf ? (await htmlProcess(`<html><head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<title>${l10n.author}. ${l10n.title}</title> <title>${l10n.author}. ${l10n.title}</title>
<meta name="author" content="${l10n.author}"/> <meta name="author" content="${l10n.author}"/>
@ -103,8 +105,10 @@ function buildDoc (lang, targets, l10n) {
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=PT+Serif&amp;family=PT+Sans&amp;family=Inconsolata"/> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=PT+Serif&amp;family=PT+Sans&amp;family=Inconsolata"/>
<style>${css}</style> <style>${css}</style>
</head><body> </head><body>
<article>${htmlContent}</article> <article>${htmlContent.join('\n')}</article>
</body></html>`; </body></html>`, {
base: __dirname
})).contents : '';
return Promise.all(['html', 'pdf', 'epub'].map((target) => { return Promise.all(['html', 'pdf', 'epub'].map((target) => {
return targets[target] ? builders[target]({ return targets[target] ? builders[target]({
@ -117,16 +121,18 @@ function buildDoc (lang, targets, l10n) {
})); }));
} }
function getStructure ({ path, l10n: { chapter }, pageBreak}) { async function getStructure ({ path, l10n, pageBreak}) {
const structure = { const structure = {
frontPage: fs.readFileSync(`${path}intro.html`, 'utf-8') + pageBreak, frontPage: fs.readFileSync(`${path}intro.html`, 'utf-8') + pageBreak,
sections: [] sections: []
}; };
let counter = 1; let counter = 1;
fs.readdirSync(path)
await fs.readdirSync(path)
.filter((p) => fs.statSync(`${path}${p}`).isDirectory()) .filter((p) => fs.statSync(`${path}${p}`).isDirectory())
.sort() .sort()
.forEach((dir, index) => { .reduce(async (p, dir, index) => {
const structure = await p;
const name = dir.split('-')[1]; const name = dir.split('-')[1];
const section = { const section = {
title: name, title: name,
@ -135,22 +141,30 @@ function getStructure ({ path, l10n: { chapter }, pageBreak}) {
} }
const subdir = `${path}${dir}/`; const subdir = `${path}${dir}/`;
fs.readdirSync(subdir) await fs.readdirSync(subdir)
.filter((p) => fs.statSync(`${subdir}${p}`).isFile() && p.indexOf('.md') == p.length - 3) .filter((p) => fs.statSync(`${subdir}${p}`).isFile() && p.indexOf('.md') == p.length - 3)
.sort() .sort()
.forEach((file) => { .reduce(async (p, file) => {
const section = await p;
const md = fs.readFileSync(`${subdir}${file}`, 'utf-8').trim(); const md = fs.readFileSync(`${subdir}${file}`, 'utf-8').trim();
const [ title, ...paragraphs ] = md.split(/\r?\n/); const content = await mdHtml(md, {
counter,
l10n,
base: __dirname
});
section.chapters.push({ section.chapters.push({
anchor: `chapter-${counter}`, anchor: content.data.anchor,
title: title.replace(/^### /, `${chapter} ${counter}. `), title: content.data.title,
content: mdHtml.makeHtml(paragraphs.join('\n')) + pageBreak content: content.contents + pageBreak
}); });
counter++; counter++;
}); return section;
}, Promise.resolve(section));
structure.sections.push(section); structure.sections.push(section);
}); return structure;
}, Promise.resolve(structure));
return structure; return structure;
} }

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -6,8 +6,13 @@
"repository": "github.com:twirl/The-API-Book", "repository": "github.com:twirl/The-API-Book",
"devDependencies": { "devDependencies": {
"puppeteer": "^5.5.0", "puppeteer": "^5.5.0",
"showdown": "^1.9.1", "epub-gen": "^0.1.0",
"epub-gen": "^0.1.0" "unified": "^9.2.0",
"remark-parse": "^8.0.3",
"remark-rehype": "^7.0.0",
"rehype-parse": "^7.0.1",
"rehype-stringify": "^8.0.0",
"image-data-uri": "^2.0.1"
}, },
"scripts": { "scripts": {
"build": "node build.js" "build": "node build.js"

View File

@ -12,12 +12,16 @@ In other words, hundreds or even thousands of different APIs must work correctly
**An API is an obligation**. A formal obligation to connect different programmable contexts. **An API is an obligation**. A formal obligation to connect different programmable contexts.
When I'm asked of an example of a well-designed API, I usually show the picture of a Roman viaduct: When I'm asked of an example of a well-designed API, I usually show the picture of a Roman aqueduct:
![The Pont-du-Gard aqueduct. Built in the 1st century AD](/src/img/pont-du-gard.jpg "The Pont-du-Gard aqueduct. Built in the 1st century AD")
###### Photo credit: [igorelick @ pixabay](https://pixabay.com/photos/pont-du-gard-france-aqueduct-bridge-3909998/)
* it interconnects two areas; * it interconnects two areas;
* backwards compatibility being broken not a single time in two thousand years. * backwards compatibility being broken not a single time in two thousand years.
What differs between a Roman viaduct and a good API is that APIs presume a contract being *programmable*. To connect two areas some *coding* is needed. The goal of this book is to help you in designing APIs which serve their purposes as solidly as a Roman viaduct does. What differs between a Roman aqueduct and a good API is that APIs presume a contract being *programmable*. To connect two areas some *coding* is needed. The goal of this book is to help you in designing APIs which serve their purposes as solidly as a Roman aqueduct does.
A viaduct also illustrates another problem of the API design: your customers are engineers themselves. You are not supplying water to end-users: suppliers are plugging their pipes to you engineering structure, building their own structures upon it. From one side, you may provide an access to the water to much more people through them, not spending your time on plugging each individual house to your network. But from other side, you can't control the quality of suppliers' solutions, and you are to be blamed every time there is a water problem caused by their incompetence. An aqueduct also illustrates another problem of the API design: your customers are engineers themselves. You are not supplying water to end-users: suppliers are plugging their pipes to you engineering structure, building their own structures upon it. From one side, you may provide an access to the water to much more people through them, not spending your time on plugging each individual house to your network. But from other side, you can't control the quality of suppliers' solutions, and you are to be blamed every time there is a water problem caused by their incompetence.
That's why designing the API implies a larger area of responsibility. **API is a multiplier to both your opportunities and mistakes**. That's why designing the API implies a larger area of responsibility. **API is a multiplier to both your opportunities and mistakes**.

View File

@ -1,5 +1,6 @@
<h1>Sergey Konstantinov<br />The API</h1> <h1>Sergey Konstantinov<br />The API</h1>
<img class="cc-by-nc-img" src="https://i.creativecommons.org/l/by-nc/4.0/88x31.png" />
<p class="cc-by-nc"> <p class="cc-by-nc">
This work is licensed under a This work is licensed under a
<a href="http://creativecommons.org/licenses/by-nc/4.0/" <a href="http://creativecommons.org/licenses/by-nc/4.0/"

BIN
src/img/pont-du-gard.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

View File

@ -1,8 +1,10 @@
const puppeteer = require('puppeteer');
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const puppeteer = require('puppeteer');
const Epub = require('epub-gen'); const Epub = require('epub-gen');
const css = fs.readFileSync('./src/epub.css', 'utf-8'); const css = fs.readFileSync(path.resolve(__dirname, '..', 'epub.css'), 'utf-8');
module.exports = { module.exports = {
html: function ({ html, path }) { html: function ({ html, path }) {

13
src/lib/html-process.js Normal file
View File

@ -0,0 +1,13 @@
const unified = require('unified');
const rehypeParse = require('rehype-parse');
const stringify = require('rehype-stringify');
const imageProcessor = require('./image-processor');
module.exports = function (contents, data) {
return unified()
.use(rehypeParse)
.use(imageProcessor)
.use(stringify)
.process({ contents, data });
}

View File

@ -0,0 +1,30 @@
const imageDataUri = require('image-data-uri');
module.exports = () => {
return (tree, file, next) => {
const images = [];
const imageSearch = (node) => {
if (node.tagName == 'img') {
images.push(node);
}
if (node.children && node.children.length) {
node.children.forEach(imageSearch);
}
}
imageSearch(tree);
Promise.all(images.map((imageNode) => {
const src = imageNode.properties.src;
return (
src.indexOf('http') == 0 ?
imageDataUri.encodeFromURL(src) :
imageDataUri.encodeFromFile(src)
).then((src) => {
imageNode.properties.src = src;
});
})).then(() => {
next();
});
}
};

53
src/lib/md-html.js Normal file
View File

@ -0,0 +1,53 @@
const path = require('path');
const unified = require('unified');
const remarkParse = require('remark-parse');
const remarkRehype = require('remark-rehype');
const rehypeStringify = require('rehype-stringify');
const chapterProcessor = () => {
return (tree, file) => {
const counter = file.data.counter;
const l10n = file.data.l10n;
file.data.anchor = file.data.anchor = `chapter-${counter}`;
const title = [
`${l10n.chapter} ${counter}`
];
const imageProcess = function (node) {
if (node.tagName == 'img' && node.properties.src.indexOf('/') == 0) {
node.properties.src = path.resolve(
file.data.base,
node.properties.src.replace(/^\//, '')
);
}
if (node.children) {
node.children.forEach(imageProcess);
}
}
tree.children.slice().forEach((node, index) => {
switch (node.tagName) {
case 'h3':
title.push(node.children[0].value);
tree.children.splice(index, 1);
break;
}
imageProcess(node);
});
file.data.title = title.join('. ');
}
}
module.exports = (contents, data) => {
return unified()
.use(remarkParse)
.use(remarkRehype)
.use(chapterProcessor)
.use(rehypeStringify)
.process({
contents,
data
});
}

View File

@ -12,12 +12,16 @@
**API — это обязательство**. Формальное обязательство связывать между собой различные программируемые контексты. **API — это обязательство**. Формальное обязательство связывать между собой различные программируемые контексты.
Когда меня просят привести пример хорошего API, я обычно показываю фотографию римского виадука: Когда меня просят привести пример хорошего API, я обычно показываю фотографию древнеримского акведука:
![Древнеримский акведук Пон-дю-Гар. Построен в I веке н.э.](/src/img/pont-du-gard.jpg "Древнеримский акведук Пон-дю-Гар. Построен в I веке н.э.")
###### Photo credit: [igorelick @ pixabay](https://pixabay.com/photos/pont-du-gard-france-aqueduct-bridge-3909998/)
* он связывает между собой две области * он связывает между собой две области
* обратная совместимость нарушена ноль раз за последние две тысячи лет. * обратная совместимость нарушена ноль раз за последние две тысячи лет.
Отличие римского виадука от хорошего API состоит лишь в том, что API предлагает _программный_ контракт. Для связывания двух областей необходимо написать некоторый _код_. Цель этой книги — помочь вам разработать API, так же хорошо выполняющий свою задачу, как и римский виадук. Отличие древнеримского акведука от хорошего API состоит лишь в том, что API предлагает _программный_ контракт. Для связывания двух областей необходимо написать некоторый _код_. Цель этой книги — помочь вам разработать API, так же хорошо выполняющий свою задачу, как и древнеримский акведук.
Виадук также хорошо иллюстрирует другую проблему разработки API: вашими пользователями являются инженеры. Вы не поставляете воду напрямую потребителю: к вашей инженерной мысли подключаются заказчики путём пристройки к ней каких-то своих инженерных конструкций. С одной стороны, вы можете обеспечить водой гораздо больше людей, нежели если бы вы сами подводили трубы к каждому крану. С другой — качество инженерных решений заказчика вы не может контролировать, и проблемы с водой, вызванные некомпетентностью подрядчика, неизбежно будут валить на вас. Акведук также хорошо иллюстрирует другую проблему разработки API: вашими пользователями являются инженеры. Вы не поставляете воду напрямую потребителю: к вашей инженерной мысли подключаются заказчики путём пристройки к ней каких-то своих инженерных конструкций. С одной стороны, вы можете обеспечить водой гораздо больше людей, нежели если бы вы сами подводили трубы к каждому крану. С другой — качество инженерных решений заказчика вы не может контролировать, и проблемы с водой, вызванные некомпетентностью подрядчика, неизбежно будут валить на вас.
Поэтому проектирование API налагает на вас несколько большую ответственность. **API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок**. Поэтому проектирование API налагает на вас несколько большую ответственность. **API является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок**.

View File

@ -13,6 +13,7 @@
Итак, предположим, что мы хотим предоставить API автоматического заказа кофе в городских кофейнях. Попробуем применить к нему этот принцип. Итак, предположим, что мы хотим предоставить API автоматического заказа кофе в городских кофейнях. Попробуем применить к нему этот принцип.
1. Зачем кому-то может потребоваться API для приготовления кофе? В чем неудобство заказа кофе через интерфейс, человек-человек или человек-машина? Зачем нужна возможность заказа машина-машина? 1. Зачем кому-то может потребоваться API для приготовления кофе? В чем неудобство заказа кофе через интерфейс, человек-человек или человек-машина? Зачем нужна возможность заказа машина-машина?
* Возможно, мы хотим решить проблему выбора и знания? Чтобы человек наиболее полно знал о доступных ему здесь и сейчас опциях. * Возможно, мы хотим решить проблему выбора и знания? Чтобы человек наиболее полно знал о доступных ему здесь и сейчас опциях.
* Возможно, мы оптимизируем время ожидания? Чтобы человеку не пришлось ждать, пока его заказ готовится. * Возможно, мы оптимизируем время ожидания? Чтобы человеку не пришлось ждать, пока его заказ готовится.
* Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказать, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины. * Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказать, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины.

View File

@ -1,6 +1,13 @@
<h1>Сергей Константинов<br/>API</h1> <h1>Сергей Константинов<br />API</h1>
<p class="cc-by-nc">Это произведение доступно по <a href="http://creativecommons.org/licenses/by-nc/4.0/">лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная</a>.</p> <img class="cc-by-nc-img" src="https://i.creativecommons.org/l/by-nc/4.0/88x31.png" />
<p class="cc-by-nc">
Это произведение доступно по
<a href="http://creativecommons.org/licenses/by-nc/4.0/"
>лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция —
Некоммерческое использование») 4.0 Всемирная</a
>.
</p>
<a <a
href="https://github.com/twirl/The-API-Book/" href="https://github.com/twirl/The-API-Book/"
@ -31,5 +38,5 @@
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor" fill="currentColor"
class="octo-body" class="octo-body"
></path></svg></a ></path></svg
> ></a>

View File

@ -4,15 +4,24 @@ html {
padding: 0; padding: 0;
} }
body { body, h6 {
font-family: 'PT Serif'; font-family: 'PT Serif';
font-size: 14pt; font-size: 14pt;
text-align: justify; text-align: justify;
} }
.cc-by-nc-img {
display: block;
float: left;
margin-top: 5px;
}
.cc-by-nc { .cc-by-nc {
background: transparent url(https://i.creativecommons.org/l/by-nc/4.0/88x31.png) 0 5px no-repeat; margin-left: 92px;
padding-left: 92px; }
.cc-by-nc::after {
clear: left;
} }
code, pre { code, pre {
@ -23,6 +32,10 @@ code {
white-space: nowrap; white-space: nowrap;
} }
p img {
max-width: 100%;
}
pre { pre {
margin: 1em 0; margin: 1em 0;
padding: 1em; padding: 1em;
@ -58,6 +71,20 @@ h1, h2, h3, h4, h5 {
page-break-after: avoid; page-break-after: avoid;
} }
h6 {
font-size: 80%;
color: darkgray;
text-align: center;
padding: 0;
margin: 0 1em 0 2em;
font-weight: normal;
}
h6 a {
color: darkgray;
font-weight: bolder;
}
h1 { h1 {
font-size: 200%; font-size: 200%;
} }