mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-01-05 10:20:22 +02:00
Images support added
This commit is contained in:
parent
5c9da3dcc6
commit
2f0002ff99
76
build.js
76
build.js
@ -1,18 +1,11 @@
|
||||
const fs = require('fs');
|
||||
const mdHtml = new (require('showdown').Converter)();
|
||||
const path = require('path');
|
||||
|
||||
const langsToBuild = process.argv[2] &&
|
||||
process.argv[2].split(',').map((s) => s.trim()) ||
|
||||
['ru', 'en'];
|
||||
const builders = require('./src/lib/builders');
|
||||
const mdHtml = require('./src/lib/md-html');
|
||||
const htmlProcess = require('./src/lib/html-process');
|
||||
|
||||
const targets = (process.argv[3] &&
|
||||
process.argv[3].split(',') ||
|
||||
['html', 'pdf', 'epub']
|
||||
).reduce((targets, arg) => {
|
||||
targets[arg.trim()] = true;
|
||||
return targets;
|
||||
}, {});
|
||||
const css = fs.readFileSync('./src/style.css', 'utf-8');
|
||||
|
||||
const l10n = {
|
||||
en: {
|
||||
@ -34,9 +27,18 @@ const l10n = {
|
||||
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(() => {
|
||||
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 structure = getStructure({
|
||||
const structure = await getStructure({
|
||||
path: `./src/${lang}/clean-copy/`,
|
||||
l10n,
|
||||
pageBreak
|
||||
@ -88,9 +90,9 @@ function buildDoc (lang, targets, l10n) {
|
||||
content.push(chapter.content);
|
||||
return content;
|
||||
}, [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"/>
|
||||
<title>${l10n.author}. ${l10n.title}</title>
|
||||
<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&family=PT+Sans&family=Inconsolata"/>
|
||||
<style>${css}</style>
|
||||
</head><body>
|
||||
<article>${htmlContent}</article>
|
||||
</body></html>`;
|
||||
<article>${htmlContent.join('\n')}</article>
|
||||
</body></html>`, {
|
||||
base: __dirname
|
||||
})).contents : '';
|
||||
|
||||
return Promise.all(['html', 'pdf', 'epub'].map((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 = {
|
||||
frontPage: fs.readFileSync(`${path}intro.html`, 'utf-8') + pageBreak,
|
||||
sections: []
|
||||
};
|
||||
let counter = 1;
|
||||
fs.readdirSync(path)
|
||||
|
||||
await fs.readdirSync(path)
|
||||
.filter((p) => fs.statSync(`${path}${p}`).isDirectory())
|
||||
.sort()
|
||||
.forEach((dir, index) => {
|
||||
.reduce(async (p, dir, index) => {
|
||||
const structure = await p;
|
||||
const name = dir.split('-')[1];
|
||||
const section = {
|
||||
title: name,
|
||||
@ -135,22 +141,30 @@ function getStructure ({ path, l10n: { chapter }, pageBreak}) {
|
||||
}
|
||||
|
||||
const subdir = `${path}${dir}/`;
|
||||
fs.readdirSync(subdir)
|
||||
await fs.readdirSync(subdir)
|
||||
.filter((p) => fs.statSync(`${subdir}${p}`).isFile() && p.indexOf('.md') == p.length - 3)
|
||||
.sort()
|
||||
.forEach((file) => {
|
||||
.reduce(async (p, file) => {
|
||||
const section = await p;
|
||||
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({
|
||||
anchor: `chapter-${counter}`,
|
||||
title: title.replace(/^### /, `${chapter} ${counter}. `),
|
||||
content: mdHtml.makeHtml(paragraphs.join('\n')) + pageBreak
|
||||
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;
|
||||
}
|
BIN
docs/API.en.epub
BIN
docs/API.en.epub
Binary file not shown.
457
docs/API.en.html
457
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.
461
docs/API.ru.html
461
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.
@ -6,8 +6,13 @@
|
||||
"repository": "github.com:twirl/The-API-Book",
|
||||
"devDependencies": {
|
||||
"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": {
|
||||
"build": "node build.js"
|
||||
|
@ -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.
|
||||
|
||||
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;
|
||||
* 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**.
|
||||
|
@ -1,5 +1,6 @@
|
||||
<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">
|
||||
This work is licensed under a
|
||||
<a href="http://creativecommons.org/licenses/by-nc/4.0/"
|
||||
|
BIN
src/img/pont-du-gard.jpg
Normal file
BIN
src/img/pont-du-gard.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 986 KiB |
@ -1,8 +1,10 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
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 = {
|
||||
html: function ({ html, path }) {
|
13
src/lib/html-process.js
Normal file
13
src/lib/html-process.js
Normal 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 });
|
||||
}
|
30
src/lib/image-processor.js
Normal file
30
src/lib/image-processor.js
Normal 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
53
src/lib/md-html.js
Normal 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
|
||||
});
|
||||
}
|
@ -12,12 +12,16 @@
|
||||
|
||||
**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 является как мультипликатором ваших возможностей, так и мультипликатором ваших ошибок**.
|
||||
|
@ -13,13 +13,14 @@
|
||||
Итак, предположим, что мы хотим предоставить API автоматического заказа кофе в городских кофейнях. Попробуем применить к нему этот принцип.
|
||||
|
||||
1. Зачем кому-то может потребоваться API для приготовления кофе? В чем неудобство заказа кофе через интерфейс, человек-человек или человек-машина? Зачем нужна возможность заказа машина-машина?
|
||||
* Возможно, мы хотим решить проблему выбора и знания? Чтобы человек наиболее полно знал о доступных ему здесь и сейчас опциях.
|
||||
* Возможно, мы оптимизируем время ожидания? Чтобы человеку не пришлось ждать, пока его заказ готовится.
|
||||
* Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказать, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины.
|
||||
|
||||
Вопрос «зачем» — самый важный из тех вопросов, которые вы должны задавать себе. Не только глобально в отношении целей всего проекта, но и локально в отношении каждого кусочка функциональности. **Если вы не можете коротко и понятно ответить на вопрос «зачем эта сущность нужна» — значит, она не нужна**.
|
||||
|
||||
Здесь и далее предположим (в целях придания нашему примеру глубины и некоторой упоротости), что мы оптимизируем все три фактора в порядке убывания важности.
|
||||
* Возможно, мы хотим решить проблему выбора и знания? Чтобы человек наиболее полно знал о доступных ему здесь и сейчас опциях.
|
||||
* Возможно, мы оптимизируем время ожидания? Чтобы человеку не пришлось ждать, пока его заказ готовится.
|
||||
* Возможно, мы хотим минимизировать ошибки? Чтобы человек получил именно то, что хотел заказать, не потеряв информацию при разговорном общении либо при настройке незнакомого интерфейса кофе-машины.
|
||||
|
||||
Вопрос «зачем» — самый важный из тех вопросов, которые вы должны задавать себе. Не только глобально в отношении целей всего проекта, но и локально в отношении каждого кусочка функциональности. **Если вы не можете коротко и понятно ответить на вопрос «зачем эта сущность нужна» — значит, она не нужна**.
|
||||
|
||||
Здесь и далее предположим (в целях придания нашему примеру глубины и некоторой упоротости), что мы оптимизируем все три фактора в порядке убывания важности.
|
||||
|
||||
2. Правда ли решаемая проблема существует? Действительно ли мы наблюдаем неравномерную загрузку кофейных автоматов по утрам? Правда ли люди страдают от того, что не могут найти поблизости нужный им латте с ореховым сиропом? Действительно ли людям важны те минуты, которые они теряют, стоя в очередях?
|
||||
|
||||
|
@ -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
|
||||
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"
|
||||
fill="currentColor"
|
||||
class="octo-body"
|
||||
></path></svg></a
|
||||
>
|
||||
></path></svg
|
||||
></a>
|
||||
|
@ -4,15 +4,24 @@ html {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
body, h6 {
|
||||
font-family: 'PT Serif';
|
||||
font-size: 14pt;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.cc-by-nc-img {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.cc-by-nc {
|
||||
background: transparent url(https://i.creativecommons.org/l/by-nc/4.0/88x31.png) 0 5px no-repeat;
|
||||
padding-left: 92px;
|
||||
margin-left: 92px;
|
||||
}
|
||||
|
||||
.cc-by-nc::after {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
@ -23,6 +32,10 @@ code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
p img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
@ -58,6 +71,20 @@ h1, h2, h3, h4, h5 {
|
||||
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 {
|
||||
font-size: 200%;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user