angular-cn/tools/dart-api-builder/dab.js

246 lines
11 KiB
JavaScript

'use strict';
const assert = require('assert-plus');
const cheerio = require('cheerio');
const Encoder = require('node-html-encoder').Encoder;
const fs = require('fs-extra');
const path = require('canonical-path');
module.exports = function dabFactory(ngIoProjPath) {
const encoder = new Encoder('entity');
// Get the functionality we need from the dgeni package by the same name.
const dartApiBuilderDgeniProjPath = 'tools/api-builder/dart-package';
const dab = require(path.resolve(ngIoProjPath, dartApiBuilderDgeniProjPath)).module;
const log = dab.logFactory[1]();
const dartPkgConfigInfo = dab.dartPkgConfigInfo[1]();
const preprocessDartDocData = dab.preprocessDartDocData[1](log, dartPkgConfigInfo);
const loadDartDocDataProcessor = dab.loadDartDocDataProcessor[1](log, dartPkgConfigInfo, preprocessDartDocData);
const apiListDataFileService = dab.apiListDataFileService[1](log, dartPkgConfigInfo);
const Array_from = dab.arrayFromIterable[1];
// Load API data, then create and save 'api-list.json'.
function loadApiDataAndSaveToApiListFile() {
const docs = [];
loadDartDocDataProcessor.$process(docs);
log.debug('Number of Dart API entries loaded:', docs.length);
var libMap = apiListDataFileService.createDataAndSaveToFile(docs);
for (let name in libMap) {
log.debug(' ', name, 'has', libMap[name].length, 'top-level entries');
}
return docs;
}
// Create and save the container's '_data.json' file.
function _createDirData(containerName, destDirPath, entries) {
const entryNames = Array_from(entries.keys()).sort();
const dataMap = Object.create(null);
entryNames.map((n) => {
const e = entries.get(n);
assert.object(e, `entry named ${n}`);
dataMap[path.basename(e.path, '.html')] = e;
});
const dataFilePath = path.resolve(destDirPath, '_data.json');
fs.writeFile(dataFilePath, JSON.stringify(dataMap, null, 2));
log.info(containerName, 'wrote', Object.keys(dataMap).length, 'entries to', dataFilePath);
}
function _adjustDocsRelativeLinks($, div) {
// Omit leading https://angular.io so links work for local test sites.
const urlToDocs = '/docs/dart/latest/';
const urlToExamples = 'http://angular-examples.github.io/';
const docsLinkList = div.find('a[href^="docs/"],a[href^="examples/"]');
docsLinkList.each((i, elt) => {
const href = $(elt).attr('href');
const matches = href.match(/(\w+)\/(.*)$/);
// TODO: support links to chapters of other languages, e.g., 'docs/ts/latest/...'.
const urlStart = matches[1] === 'docs' ? urlToDocs : urlToExamples;
const absHref = urlStart + matches[2];
log.info(`Found angular.io relative link: ${href} --> ${absHref}`);
$(elt).attr('href', absHref);
});
}
function _insertExampleFragments(enclosedByName, eltId, $, div) {
const fragDirBase = path.join(dartPkgConfigInfo.ngIoDartApiDocPath, '../../../_fragments/');
const exList = div.find('p:contains("{@example")');
exList.each((i, elt) => {
const text = $(elt).text();
log.debug(`Found example: ${enclosedByName} ${eltId}`, text);
const matches = text.match(/^\s*{@example\s+([^\s]+)(\s+region=[\'\"]?([-\w]+)[\'\"]?)?\s*}([\s\S]*)$/);
if (!matches) {
log.warn(enclosedByName, eltId, 'has an invalidly formed @example tag:', text);
return true;
}
// const [, exRelPath, /*regionTagAndValue*/, region, rest] = matches;
const rest = matches[4].trim();
if (rest) log.warn(enclosedByName, eltId, '@example must be the only element in a paragraph, but found:', text);
const exRelPath = matches[1];
const region = matches[3];
let exRelPathParts = path.dirname(exRelPath).split(path.sep);
let fragDir;
if (exRelPathParts[0] === 'docs') {
// Path is to a docs example, not an API example.
const exampleName = exRelPathParts[1];
fragDir = path.join(fragDirBase, exampleName, 'dart');
exRelPathParts = exRelPathParts.slice(2);
} else {
fragDir = path.join(fragDirBase, '_api');
}
const extn = path.extname(exRelPath);
const baseName = path.basename(exRelPath, extn);
const fileNameNoExt = baseName + (region ? `-${region}` : '')
const exFragPath = path.resolve(fragDir, ...exRelPathParts, `${fileNameNoExt}${extn}.md`);
if (!fs.existsSync(exFragPath)) {
log.warn('Fragment not found:', exFragPath);
return true;
}
$(elt).empty();
const md = fs.readFileSync(exFragPath, 'utf8');
const codeElt = _extractAndWrapInCodeTags(md);
$(elt).html(codeElt);
log.silly('Fragment code in html:', $(elt).html());
});
}
function _extractAndWrapInCodeTags(md) {
const lines = md.split('\n');
// Drop first and last lines that are the code markdown tripple ticks (and last \n):
lines.shift();
while (lines && lines.pop().trim() !== '```') {}
const code = lines.map((line) => encoder.htmlEncode(line)).join('\n');
// TS uses format="linenums"; removing that for now.
return `<code-example language="dart">${code}\n</code-example>`;
}
function _createEntryJadeFile(e, destDirPath) {
const htmlPagePath = path.resolve(dartPkgConfigInfo.ngDartDocPath, e.path);
if (!fs.existsSync(htmlPagePath)) {
log.warn('Entry', e.name, ': expected to find file but didn\'t', htmlPagePath);
return;
}
const html = fs.readFileSync(htmlPagePath, 'utf8');
log.debug('Reading (and then deleting)', html.length, 'chars from', htmlPagePath);
const $ = cheerio.load(html);
const div = $('div.body.container');
$('div.sidebar-offcanvas-left').remove();
const baseNameNoExtn = path.basename(e.path, '.html');
_adjustDocsRelativeLinks($, div);
_insertExampleFragments(e.enclosedByQualifiedName, baseNameNoExtn, $, div);
const outFileNoExtn = path.join(destDirPath, baseNameNoExtn);
const depth = path.dirname(e.path).split('/').length;
assert(depth === 1 || depth == 2, 'depth ' + depth);
const jadeFilePath = path.resolve(outFileNoExtn + '.jade');
const breadcrumbs = $('header > nav ol.breadcrumbs');
fs.writeFileSync(jadeFilePath, apiEntryJadeTemplate(depth, breadcrumbs, div));
// In case harp cached the .html version, remove it since it will be generated.
try {
fs.unlinkSync(path.resolve(outFileNoExtn + '.html'));
} catch (err) {
if (e.enclosedBy && e.enclosedBy.type === 'class' &&
e.enclosedBy.name.toLowerCase() === e.name.toLowerCase()) {
// Do nothing since this is a known bug with dartdoc:
// https://github.com/dart-lang/dartdoc/issues/1196
} else {
console.error(err);
console.error(`Output path: ${destDirPath}`);
console.error(`Entity: ${e}`);
console.error(err.stack);
throw err;
}
}
log.debug(' ', e.enclosedByQualifiedName, 'entry', e.name, 'wrote to ', jadeFilePath);
}
function _createJadeFiles(containerName, destDirPath, entries) {
let numApiPagesWritten = 0;
for (let name of entries.keys()) {
_createEntryJadeFile(entries.get(name), destDirPath);
numApiPagesWritten++
}
log.info(containerName, 'created', numApiPagesWritten, 'Jade entry files.');
return numApiPagesWritten;
}
function createApiDataAndJadeFiles(docs) {
let numApiPagesWritten = 0;
let map = apiListDataFileService.containerToEntryMap;
for (let name of map.keys()) {
if (!name) continue; // skip package-level
let destDirPath = path.resolve(dartPkgConfigInfo.ngIoDartApiDocPath, name);
let entries;
if (!fs.existsSync(destDirPath)) {
log.error(`Dartdoc API folder not found:`, destDirPath);
} else if ((entries = map.get(name)).size > 0) {
_createDirData(name, destDirPath, entries);
numApiPagesWritten += _createJadeFiles(name, destDirPath, entries);
}
}
return numApiPagesWritten;
}
const _self = {
Array_from: Array_from,
apiEntryJadeTemplate: apiEntryJadeTemplate,
apiListDataFileService: apiListDataFileService,
loadApiDataAndSaveToApiListFile: loadApiDataAndSaveToApiListFile,
createApiDataAndJadeFiles: createApiDataAndJadeFiles,
dartPkgConfigInfo: dartPkgConfigInfo,
loadDartDocDataProcessor: loadDartDocDataProcessor,
log: log,
preprocessDartDocData: preprocessDartDocData,
};
Object.freeze(_self);
return _self;
};
function _indentedEltHtml($elt, i, filterFnOpt) {
let lines = $elt.html().split('\n');
if (filterFnOpt) lines = lines.filter(filterFnOpt);
const indent = ' '.substring(0,i);
return lines.map((line) => `${indent}| ${line}`).join('\n');
}
function apiEntryJadeTemplate(baseHrefDepth, $breadcrumbs, $mainDiv) {
const baseHref = path.join(...Array(baseHrefDepth).fill('..'));
// TODO/investigate: for some reason $breadcrumbs.html() is missing the <ol></ol>. We add it back in the template below.
const breadcrumbs = _indentedEltHtml($breadcrumbs, 6, (line) => !line.match(/^\s*$/));
const mainDivHtml = _indentedEltHtml($mainDiv, 4);
// WARNING: since the following is Jade, indentation is significant.
const result = `
extends ${baseHref}/../../../_layout-dart-api
include ${baseHref}/../_util-fns
block var-def
//- FIXME: a CSS expert needs to figure out why the header CSS needs to be patched for Dart.
//- This enables the patch:
- var fixHeroCss = 1;
block head-extra
// generated Dart API page template: head-extra
//- <base> is required because all the links in dartdoc generated pages are "pseudo-absolute"
base(href="${baseHref}")
link(rel='stylesheet' href='https://fonts.googleapis.com/css?family=Source+Code+Pro|Roboto:500,400italic,300,400' type='text/css')
link(rel="stylesheet" href="static-assets/prettify.css")
link(rel="stylesheet" href="static-assets/css/bootstrap.min.css")
link(rel="stylesheet" href="static-assets/styles.css")
block breadcrumbs
// generated Dart API page template: breadcrumbs
nav.dropdown
ol.breadcrumbs.gt-separated.hidden-xs
${breadcrumbs}
block main-content
// generated Dart API page template: main-content: start
div.dart-api-entry-main
${mainDivHtml}
// generated Dart API page template: main-content: end
`;
return result;
}