'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}\n`; } 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
    . 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 //- 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; }