template/template.js (168 lines of code) (raw):
/**
* @license
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const {Readable} = require('stream');
const {promisify} = require('util');
const recursiveCopy = promisify(require('recursive-copy'));
const mkdir = promisify(fs.mkdir);
const readdir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
const stat = promisify(fs.stat);
module.exports = templateSource => {
const apply = (rawItr, lang, initialJsState) => {
/*
* We apply the template by walking a stream for the template and a stream
* for the raw page in parallel. We do this instead of pulling everything
* into memory and manipulating it to keep the memory usage small even when
* the template or raw page are very large. We expect the most memory this
* can use is 3x the sum of the sum of highWaterMark of both streams.
*/
const Gatherer = async (name, itr) => {
let chunk = '';
const nextChunk = async preserve => {
const result = await itr.next();
if (result.done) {
return false;
}
if (preserve) {
/* If we're looking for a marker then we need to keep some characters
* at the end of the chunk in case the marker is on the edge. */
const slice = Math.max(0, chunk.length - preserve);
chunk = chunk.slice(slice) + result.value;
} else {
chunk = result.value;
}
return true;
};
if (!await nextChunk()) {
throw new Error(`${name} didn't have any data`)
}
const gather = async function* (marker) {
let index;
while ((index = chunk.indexOf(marker)) < 0) {
const slice = chunk.length - marker.length;
if (slice > 0) {
yield chunk.slice(0, slice);
}
if (!await nextChunk(marker.length)) {
throw new Error(`Couldn't find ${marker} in ${name}:\n${chunk}`);
}
}
yield chunk.slice(0, index);
chunk = chunk.slice(index + marker.length);
};
const dump = async marker => {
let index;
while ((index = chunk.indexOf(marker)) < 0) {
if (!await nextChunk(marker.length)) {
throw new Error(`Couldn't find ${marker} in ${name}:\n${chunk}`);
}
}
chunk = chunk.slice(index + marker.length);
}
async function* remaining() {
yield chunk;
while (await nextChunk()) {
yield chunk;
}
}
return {
gather: gather,
dump: dump,
remaining: remaining,
};
};
async function* asyncApply() {
const templateStream = templateSource();
try {
const template = await Gatherer('template', templateStream[Symbol.asyncIterator]());
yield* template.gather("<!-- DOCS PREHEAD -->");
const raw = await Gatherer('raw', rawItr);
await raw.dump("<head>");
yield* raw.gather("</head>");
yield* template.gather("<!-- DOCS LANG -->");
yield `lang="${lang}"`;
yield* template.gather("<!-- DOCS BODY -->");
await raw.dump("<body>");
yield* raw.gather("</body>");
yield* template.gather("<!-- DOCS FINAL -->");
yield `<script type="text/javascript">
window.initial_state = ${initialJsState}</script>`;
yield* template.remaining();
} catch (err) {
templateStream.destroy(err);
throw err;
}
}
// TODO this defaults to object mode and maybe shouldn't.
return Readable.from(asyncApply());
};
const buildInitialJsStateFromFile = async alternativesSummaryFile => {
let alternativesSummary;
try {
alternativesSummary = await readFile(alternativesSummaryFile, {
encoding: "utf8",
});
} catch (err) {
if (err.errno === -2) { // ENOENT
return "{}";
}
throw err;
}
return JSON.stringify(module.exports.buildInitialJsState(JSON.parse(alternativesSummary)));
};
const applyToDir = async (sourcePath, destPath, tocMode) => {
const langFile = `${sourcePath}/lang`;
const alternativesSummaryFile = `${sourcePath}/alternatives_summary.json`;
const alternativesReportFile = `${sourcePath}/alternatives_report.json`;
const tocFile = `${sourcePath}/toc.html`;
const initialJsState = await buildInitialJsStateFromFile(alternativesSummaryFile);
const lang = (await readFile(langFile, {
encoding: "utf8",
})).trim();
const entries = await readdir(sourcePath);
await mkdir(destPath, {recursive: true});
for (var e = 0; e < entries.length; e++) {
const basename = entries[e];
const source = path.join(sourcePath, basename);
const dest = path.join(destPath, basename);
if (source === langFile) {
continue;
}
if (source === alternativesSummaryFile) {
continue;
}
if (source === alternativesReportFile) {
continue;
}
const sourceStat = await stat(source);
if (sourceStat.isDirectory()) {
/*
* Usually books are built to empty directories and any
* subdirectories contain images or snippets and should be copied
* wholesale into the templated directory. But the book's
* multi-version table of contents is different because it is built
* to the root directory of all book versions so subdirectories are
* other books! Copying them would overwrite the templates book
* files with untemplated book files. That'd be bad!
*/
if (!tocMode) {
await recursiveCopy(source, dest);
}
continue;
}
if (source === tocFile || !basename.endsWith(".html")) {
await recursiveCopy(source, dest);
continue;
}
const raw = fs.createReadStream(source, {encoding: 'UTF-8'});
const write = fs.createWriteStream(dest, {encoding: 'UTF-8'});
await new Promise((resolve, reject) => {
const out = apply(raw[Symbol.asyncIterator](), lang, initialJsState);
write.on("close", resolve);
write.on("error", reject);
// out.on("error", write.destroy) doesn't properly forward the error!
out.on("error", err => write.destroy(err));
out.pipe(write);
}).finally(() => raw.close());
};
};
return {
apply: apply,
applyToDir: applyToDir,
};
};
module.exports.buildInitialJsState = alternativesSummary => {
const result = {};
result.alternatives = {};
for (var sourceLang in alternativesSummary) {
const forSourceLang = alternativesSummary[sourceLang];
result.alternatives[sourceLang] = {};
for (var altLang in forSourceLang.alternatives) {
result.alternatives[sourceLang][altLang] = {
hasAny: forSourceLang.alternatives[altLang].found > 0,
};
}
}
return result;
};