238 lines
10 KiB
JavaScript
238 lines
10 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 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="static-assets/styles.css")
|
|
|
|
block breadcrumbs
|
|
// generated Dart API page template: breadcrumbs
|
|
.banner
|
|
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;
|
|
}
|