build.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  1. /* global module require process */
  2. /* eslint no-undef: "error" */
  3. /* eslint no-console: "off" */
  4. 'use strict';
  5. module.exports = function(settings) { // wrapper in case we're in module_context mode
  6. const cache = new (require('inmemfilecache'))();
  7. const Feed = require('feed').Feed;
  8. const fs = require('fs');
  9. const glob = require('glob');
  10. const Handlebars = require('handlebars');
  11. const hanson = require('hanson');
  12. const marked = require('marked');
  13. const path = require('path');
  14. const Promise = require('promise');
  15. const sitemap = require('sitemap');
  16. const utils = require('./utils');
  17. const moment = require('moment');
  18. const url = require('url');
  19. //process.title = 'build';
  20. let numErrors = 0;
  21. function error(...args) {
  22. ++numErrors;
  23. console.error(...args);
  24. }
  25. const executeP = Promise.denodeify(utils.execute);
  26. marked.setOptions({
  27. rawHtml: true,
  28. //pedantic: true,
  29. });
  30. function applyObject(src, dst) {
  31. Object.keys(src).forEach(function(key) {
  32. dst[key] = src[key];
  33. });
  34. return dst;
  35. }
  36. function mergeObjects() {
  37. const merged = {};
  38. Array.prototype.slice.call(arguments).forEach(function(src) {
  39. applyObject(src, merged);
  40. });
  41. return merged;
  42. }
  43. function readFile(fileName) {
  44. return cache.readFileSync(fileName, 'utf-8');
  45. }
  46. function writeFileIfChanged(fileName, content) {
  47. if (fs.existsSync(fileName)) {
  48. const old = readFile(fileName);
  49. if (content === old) {
  50. return;
  51. }
  52. }
  53. fs.writeFileSync(fileName, content);
  54. console.log('Wrote: ' + fileName); // eslint-disable-line
  55. }
  56. function copyFile(src, dst) {
  57. writeFileIfChanged(dst, readFile(src));
  58. }
  59. function replaceParams(str, params) {
  60. const template = Handlebars.compile(str);
  61. if (Array.isArray(params)) {
  62. params = mergeObjects.apply(null, params.slice().reverse());
  63. }
  64. return template(params);
  65. }
  66. function encodeParams(params) {
  67. const values = Object.values(params).filter(v => v);
  68. if (!values.length) {
  69. return '';
  70. }
  71. return '&' + Object.entries(params).map((kv) => {
  72. return `${encodeURIComponent(kv[0])}=${encodeURIComponent(kv[1])}`;
  73. }).join('&');
  74. }
  75. function encodeQuery(query) {
  76. if (!query) {
  77. return '';
  78. }
  79. return '?' + query.split('&').map(function(pair) {
  80. return pair.split('=').map(function(kv) {
  81. return encodeURIComponent(decodeURIComponent(kv));
  82. }).join('=');
  83. }).join('&');
  84. }
  85. function encodeUrl(src) {
  86. const u = url.parse(src);
  87. u.search = encodeQuery(u.query);
  88. return url.format(u);
  89. }
  90. function TemplateManager() {
  91. const templates = {};
  92. this.apply = function(filename, params) {
  93. let template = templates[filename];
  94. if (!template) {
  95. template = Handlebars.compile(readFile(filename));
  96. templates[filename] = template;
  97. }
  98. if (Array.isArray(params)) {
  99. params = mergeObjects.apply(null, params.slice().reverse());
  100. }
  101. return template(params);
  102. };
  103. }
  104. const templateManager = new TemplateManager();
  105. Handlebars.registerHelper('include', function(filename, options) {
  106. let context;
  107. if (options && options.hash && options.hash.filename) {
  108. const varName = options.hash.filename;
  109. filename = options.data.root[varName];
  110. context = options.hash;
  111. } else {
  112. context = options.data.root;
  113. }
  114. return templateManager.apply(filename, context);
  115. });
  116. Handlebars.registerHelper('example', function(options) {
  117. options.hash.width = options.hash.width ? 'width: ' + options.hash.width + 'px;' : '';
  118. options.hash.height = options.hash.height ? 'height: ' + options.hash.height + 'px;' : '';
  119. options.hash.caption = options.hash.caption || options.data.root.defaultExampleCaption;
  120. options.hash.examplePath = options.data.root.examplePath;
  121. options.hash.encodedUrl = encodeURIComponent(encodeUrl(options.hash.url));
  122. options.hash.url = encodeUrl(options.hash.url);
  123. options.hash.params = encodeParams({
  124. startPane: options.hash.startPane,
  125. });
  126. return templateManager.apply('build/templates/example.template', options.hash);
  127. });
  128. Handlebars.registerHelper('diagram', function(options) {
  129. options.hash.width = options.hash.width || '400';
  130. options.hash.height = options.hash.height || '300';
  131. options.hash.examplePath = options.data.root.examplePath;
  132. options.hash.className = options.hash.className || '';
  133. options.hash.url = encodeUrl(options.hash.url);
  134. return templateManager.apply('build/templates/diagram.template', options.hash);
  135. });
  136. Handlebars.registerHelper('image', function(options) {
  137. options.hash.examplePath = options.data.root.examplePath;
  138. options.hash.className = options.hash.className || '';
  139. options.hash.caption = options.hash.caption || undefined;
  140. if (options.hash.url.substring(0, 4) === 'http') {
  141. options.hash.examplePath = '';
  142. }
  143. return templateManager.apply('build/templates/image.template', options.hash);
  144. });
  145. Handlebars.registerHelper('selected', function(options) {
  146. const key = options.hash.key;
  147. const value = options.hash.value;
  148. const re = options.hash.re;
  149. const sub = options.hash.sub;
  150. const a = this[key];
  151. let b = options.data.root[value];
  152. if (re) {
  153. const r = new RegExp(re);
  154. b = b.replace(r, sub);
  155. }
  156. return a === b ? 'selected' : '';
  157. });
  158. function slashify(s) {
  159. return s.replace(/\\/g, '/');
  160. }
  161. function articleFilter(f) {
  162. return !process.env['ARTICLE_FILTER'] || f.indexOf(process.env['ARTICLE_FILTER']) >= 0;
  163. }
  164. const Builder = function(outBaseDir, options) {
  165. const g_articlesByLang = {};
  166. let g_articles = [];
  167. let g_langInfo;
  168. const g_langDB = {};
  169. const g_outBaseDir = outBaseDir;
  170. const g_origPath = options.origPath;
  171. // This are the english articles.
  172. const g_origArticles = glob.
  173. sync(path.join(g_origPath, '*.md'))
  174. .map(a => path.basename(a))
  175. .filter(a => a !== 'index.md')
  176. .filter(articleFilter);
  177. const extractHeader = (function() {
  178. const headerRE = /([A-Z0-9_-]+): (.*?)$/i;
  179. return function(content) {
  180. const metaData = { };
  181. const lines = content.split('\n');
  182. for (;;) {
  183. const line = lines[0].trim();
  184. const m = headerRE.exec(line);
  185. if (!m) {
  186. break;
  187. }
  188. metaData[m[1].toLowerCase()] = m[2];
  189. lines.shift();
  190. }
  191. return {
  192. content: lines.join('\n'),
  193. headers: metaData,
  194. };
  195. };
  196. }());
  197. const parseMD = function(content) {
  198. return extractHeader(content);
  199. };
  200. const loadMD = function(contentFileName) {
  201. const content = cache.readFileSync(contentFileName, 'utf-8');
  202. return parseMD(content);
  203. };
  204. function extractHandlebars(content) {
  205. const tripleRE = /\{\{\{.*?\}\}\}/g;
  206. const doubleRE = /\{\{\{.*?\}\}\}/g;
  207. let numExtractions = 0;
  208. const extractions = {
  209. };
  210. function saveHandlebar(match) {
  211. const id = '==HANDLEBARS_ID_' + (++numExtractions) + '==';
  212. extractions[id] = match;
  213. return id;
  214. }
  215. content = content.replace(tripleRE, saveHandlebar);
  216. content = content.replace(doubleRE, saveHandlebar);
  217. return {
  218. content: content,
  219. extractions: extractions,
  220. };
  221. }
  222. function insertHandlebars(info, content) {
  223. const handlebarRE = /==HANDLEBARS_ID_\d+==/g;
  224. function restoreHandlebar(match) {
  225. const value = info.extractions[match];
  226. if (value === undefined) {
  227. throw new Error('no match restoring handlebar for: ' + match);
  228. }
  229. return value;
  230. }
  231. content = content.replace(handlebarRE, restoreHandlebar);
  232. return content;
  233. }
  234. function isSameDomain(url, pageUrl) {
  235. const fdq1 = new URL(pageUrl);
  236. const fdq2 = new URL(url, pageUrl);
  237. return fdq1.origin === fdq2.origin;
  238. }
  239. function getUrlPath(url) {
  240. // yes, this is a hack
  241. const q = url.indexOf('?');
  242. return q >= 0 ? url.substring(0, q) : url;
  243. }
  244. // Try top fix relative links. This *should* only
  245. // happen in translations
  246. const iframeLinkRE = /(<iframe[\s\S]*?\s+src=")(.*?)(")/g;
  247. const imgLinkRE = /(<img[\s\S]*?\s+src=")(.*?)(")/g;
  248. const aLinkRE = /(<a[\s\S]*?\s+href=")(.*?)(")/g;
  249. const mdLinkRE = /(\[[\s\S]*?\]\()(.*?)(\))/g;
  250. const handlebarLinkRE = /({{{.*?\s+url=")(.*?)(")/g;
  251. const linkREs = [
  252. iframeLinkRE,
  253. imgLinkRE,
  254. aLinkRE,
  255. mdLinkRE,
  256. handlebarLinkRE,
  257. ];
  258. function hackRelLinks(content, pageUrl) {
  259. // console.log('---> pageUrl:', pageUrl);
  260. function fixRelLink(m, prefix, url, suffix) {
  261. if (isSameDomain(url, pageUrl)) {
  262. // a link that starts with "../" should be "../../" if it's in a translation
  263. // a link that starts with "resources" should be "../resources" if it's in a translation
  264. if (url.startsWith('../') ||
  265. url.startsWith('resources')) {
  266. // console.log(' url:', url);
  267. return `${prefix}../${url}${suffix}`;
  268. }
  269. }
  270. return m;
  271. }
  272. return content
  273. .replace(imgLinkRE, fixRelLink)
  274. .replace(aLinkRE, fixRelLink)
  275. .replace(iframeLinkRE, fixRelLink);
  276. }
  277. /**
  278. * Get all the local urls based on a regex that has <prefix><url><suffix>
  279. */
  280. function getUrls(regex, str) {
  281. const links = new Set();
  282. let m;
  283. do {
  284. m = regex.exec(str);
  285. if (m && m[2][0] !== '#' && isSameDomain(m[2], 'http://example.com/a/b/c/d')) {
  286. links.add(getUrlPath(m[2]));
  287. }
  288. } while (m);
  289. return links;
  290. }
  291. /**
  292. * Get all the local links in content
  293. */
  294. function getLinks(content) {
  295. return new Set(linkREs.map(re => [...getUrls(re, content)]).flat());
  296. }
  297. function fixUrls(regex, content, origLinks) {
  298. return content.replace(regex, (m, prefix, url, suffix) => {
  299. const q = url.indexOf('?');
  300. const urlPath = q >= 0 ? url.substring(0, q) : url;
  301. const urlQuery = q >= 0 ? url.substring(q) : '';
  302. if (!origLinks.has(urlPath) &&
  303. isSameDomain(urlPath, 'https://foo.com/a/b/c/d.html') &&
  304. !(/\/..\/^/.test(urlPath)) && // hacky test for link to main page. Example /webgl/lessons/ja/
  305. urlPath[0] !== '#') { // test for same page anchor -- bad test :(
  306. for (const origLink of origLinks) {
  307. if (urlPath.endsWith(origLink)) {
  308. const newUrl = `${origLink}${urlQuery}`;
  309. console.log(' fixing:', url, 'to', newUrl);
  310. return `${prefix}${newUrl}${suffix}`;
  311. }
  312. }
  313. error('could not fix:', url);
  314. }
  315. return m;
  316. });
  317. }
  318. const applyTemplateToContent = function(templatePath, contentFileName, outFileName, opt_extra, data) {
  319. // Call prep's Content which parses the HTML. This helps us find missing tags
  320. // should probably call something else.
  321. //Convert(md_content)
  322. const relativeOutName = slashify(outFileName).substring(g_outBaseDir.length);
  323. const pageUrl = `${settings.baseUrl}${relativeOutName}`;
  324. const metaData = data.headers;
  325. const content = data.content;
  326. //console.log(JSON.stringify(metaData, undefined, ' '));
  327. const info = extractHandlebars(content);
  328. let html = marked(info.content);
  329. // HACK! :-(
  330. if (opt_extra && opt_extra.home && opt_extra.home.length > 1) {
  331. html = hackRelLinks(html, pageUrl);
  332. }
  333. html = insertHandlebars(info, html);
  334. html = replaceParams(html, [opt_extra, g_langInfo]);
  335. const pathRE = new RegExp(`^\\/${settings.rootFolder}\\/lessons\\/$`);
  336. const langs = Object.keys(g_langDB).map((name) => {
  337. const lang = g_langDB[name];
  338. const url = slashify(path.join(lang.basePath, path.basename(outFileName)))
  339. .replace('index.html', '')
  340. .replace(pathRE, '/');
  341. return {
  342. lang: lang.lang,
  343. language: lang.language,
  344. url: url,
  345. };
  346. });
  347. metaData['content'] = html;
  348. metaData['langs'] = langs;
  349. metaData['src_file_name'] = slashify(contentFileName);
  350. metaData['dst_file_name'] = relativeOutName;
  351. metaData['basedir'] = '';
  352. metaData['toc'] = opt_extra.toc;
  353. metaData['templateOptions'] = opt_extra.templateOptions;
  354. metaData['langInfo'] = g_langInfo;
  355. metaData['url'] = pageUrl;
  356. metaData['relUrl'] = relativeOutName;
  357. metaData['screenshot'] = `${settings.baseUrl}/${settings.rootFolder}/lessons/resources/${settings.siteThumbnail}`;
  358. const basename = path.basename(contentFileName, '.md');
  359. ['.jpg', '.png'].forEach(function(ext) {
  360. const filename = path.join(settings.rootFolder, 'lessons', 'screenshots', basename + ext);
  361. if (fs.existsSync(filename)) {
  362. metaData['screenshot'] = `${settings.baseUrl}/${settings.rootFolder}/lessons/screenshots/${basename}${ext}`;
  363. }
  364. });
  365. const output = templateManager.apply(templatePath, metaData);
  366. writeFileIfChanged(outFileName, output);
  367. return metaData;
  368. };
  369. const applyTemplateToFile = function(templatePath, contentFileName, outFileName, opt_extra) {
  370. console.log('processing: ', contentFileName); // eslint-disable-line
  371. opt_extra = opt_extra || {};
  372. const data = loadMD(contentFileName);
  373. const metaData = applyTemplateToContent(templatePath, contentFileName, outFileName, opt_extra, data);
  374. g_articles.push(metaData);
  375. };
  376. const applyTemplateToFiles = function(templatePath, filesSpec, extra) {
  377. const files = glob
  378. .sync(filesSpec)
  379. .sort()
  380. .filter(articleFilter);
  381. files.forEach(function(fileName) {
  382. const ext = path.extname(fileName);
  383. const baseName = fileName.substr(0, fileName.length - ext.length);
  384. const outFileName = path.join(outBaseDir, baseName + '.html');
  385. applyTemplateToFile(templatePath, fileName, outFileName, extra);
  386. });
  387. };
  388. const addArticleByLang = function(article, lang) {
  389. const filename = path.basename(article.dst_file_name);
  390. let articleInfo = g_articlesByLang[filename];
  391. const url = `${settings.baseUrl}${article.dst_file_name}`;
  392. if (!articleInfo) {
  393. articleInfo = {
  394. url: url,
  395. changefreq: 'monthly',
  396. links: [],
  397. };
  398. g_articlesByLang[filename] = articleInfo;
  399. }
  400. articleInfo.links.push({
  401. url: url,
  402. lang: lang,
  403. });
  404. };
  405. const getLanguageSelection = function(lang) {
  406. const lessons = lang.lessons;
  407. const langInfo = hanson.parse(fs.readFileSync(path.join(lessons, 'langinfo.hanson'), {encoding: 'utf8'}));
  408. langInfo.langCode = langInfo.langCode || lang.lang;
  409. langInfo.home = lang.home;
  410. g_langDB[lang.lang] = {
  411. lang: lang.lang,
  412. language: langInfo.language,
  413. basePath: '/' + lessons,
  414. langInfo: langInfo,
  415. };
  416. };
  417. this.preProcess = function(langs) {
  418. langs.forEach(getLanguageSelection);
  419. };
  420. this.process = function(options) {
  421. console.log('Processing Lang: ' + options.lang); // eslint-disable-line
  422. g_articles = [];
  423. g_langInfo = g_langDB[options.lang].langInfo;
  424. applyTemplateToFiles(options.template, path.join(options.lessons, settings.lessonGrep), options);
  425. const articlesFilenames = g_articles.map(a => path.basename(a.src_file_name));
  426. // should do this first was easier to add here
  427. if (options.lang !== 'en') {
  428. const existing = g_origArticles.filter(name => articlesFilenames.indexOf(name) >= 0);
  429. existing.forEach((name) => {
  430. const origMdFilename = path.join(g_origPath, name);
  431. const transMdFilename = path.join(g_origPath, options.lang, name);
  432. const origLinks = getLinks(loadMD(origMdFilename).content);
  433. const transLinks = getLinks(loadMD(transMdFilename).content);
  434. if (process.env['ARTICLE_VERBOSE']) {
  435. console.log('---[', transMdFilename, ']---');
  436. console.log('origLinks: ---\n ', [...origLinks].join('\n '));
  437. console.log('transLinks: ---\n ', [...transLinks].join('\n '));
  438. }
  439. let show = true;
  440. transLinks.forEach((link) => {
  441. if (!origLinks.has(link)) {
  442. if (show) {
  443. show = false;
  444. error('---[', transMdFilename, ']---');
  445. }
  446. error(' link:[', link, '] not found in English file');
  447. }
  448. });
  449. if (!show && process.env['ARTICLE_FIX']) {
  450. // there was an error, try to auto-fix
  451. let fixedMd = fs.readFileSync(transMdFilename, {encoding: 'utf8'});
  452. linkREs.forEach((re) => {
  453. fixedMd = fixUrls(re, fixedMd, origLinks);
  454. });
  455. fs.writeFileSync(transMdFilename, fixedMd);
  456. }
  457. });
  458. }
  459. // generate place holders for non-translated files
  460. const missing = g_origArticles.filter(name => articlesFilenames.indexOf(name) < 0);
  461. missing.forEach(name => {
  462. const ext = path.extname(name);
  463. const baseName = name.substr(0, name.length - ext.length);
  464. const outFileName = path.join(outBaseDir, options.lessons, baseName + '.html');
  465. const data = Object.assign({}, loadMD(path.join(g_origPath, name)));
  466. data.content = g_langInfo.missing;
  467. const extra = {
  468. origLink: '/' + slashify(path.join(g_origPath, baseName + '.html')),
  469. toc: options.toc,
  470. };
  471. console.log(' generating missing:', outFileName); // eslint-disable-line
  472. applyTemplateToContent(
  473. 'build/templates/missing.template',
  474. path.join(options.lessons, 'langinfo.hanson'),
  475. outFileName,
  476. extra,
  477. data);
  478. });
  479. function utcMomentFromGitLog(result, filename, timeType) {
  480. const dateStr = result.stdout.split('\n')[0].trim();
  481. const utcDateStr = dateStr
  482. .replace(/"/g, '') // WTF to these quotes come from!??!
  483. .replace(' ', 'T')
  484. .replace(' ', '')
  485. .replace(/(\d\d)$/, ':$1');
  486. const m = moment.utc(utcDateStr);
  487. if (m.isValid()) {
  488. return m;
  489. }
  490. const stat = fs.statSync(filename);
  491. return moment(stat[timeType]);
  492. }
  493. const tasks = g_articles.map((article) => {
  494. return function() {
  495. return executeP('git', [
  496. 'log',
  497. '--format="%ci"',
  498. '--name-only',
  499. '--diff-filter=A',
  500. article.src_file_name,
  501. ]).then((result) => {
  502. article.dateAdded = utcMomentFromGitLog(result, article.src_file_name, 'ctime');
  503. });
  504. };
  505. }).concat(g_articles.map((article) => {
  506. return function() {
  507. return executeP('git', [
  508. 'log',
  509. '--format="%ci"',
  510. '--name-only',
  511. '--max-count=1',
  512. article.src_file_name,
  513. ]).then((result) => {
  514. article.dateModified = utcMomentFromGitLog(result, article.src_file_name, 'mtime');
  515. });
  516. };
  517. }));
  518. return tasks.reduce(function(cur, next){
  519. return cur.then(next);
  520. }, Promise.resolve()).then(function() {
  521. let articles = g_articles.filter(function(article) {
  522. return article.dateAdded !== undefined;
  523. });
  524. articles = articles.sort(function(a, b) {
  525. return b.dateAdded - a.dateAdded;
  526. });
  527. if (articles.length) {
  528. const feed = new Feed({
  529. title: g_langInfo.title,
  530. description: g_langInfo.description,
  531. link: g_langInfo.link,
  532. image: `${settings.baseUrl}/${settings.rootFolder}/lessons/resources/${settings.siteThumbnail}`,
  533. date: articles[0].dateModified.toDate(),
  534. published: articles[0].dateModified.toDate(),
  535. updated: articles[0].dateModified.toDate(),
  536. author: {
  537. name: `${settings.siteName} Contributors`,
  538. link: `${settings.baseUrl}/contributors.html`,
  539. },
  540. });
  541. articles.forEach(function(article) {
  542. feed.addItem({
  543. title: article.title,
  544. link: `${settings.baseUrl}${article.dst_file_name}`,
  545. description: '',
  546. author: [
  547. {
  548. name: `${settings.siteName} Contributors`,
  549. link: `${settings.baseUrl}/contributors.html`,
  550. },
  551. ],
  552. // contributor: [
  553. // ],
  554. date: article.dateModified.toDate(),
  555. published: article.dateAdded.toDate(),
  556. // image: posts[key].image
  557. });
  558. addArticleByLang(article, options.lang);
  559. });
  560. try {
  561. const outPath = path.join(g_outBaseDir, options.lessons, 'atom.xml');
  562. console.log('write:', outPath); // eslint-disable-line
  563. writeFileIfChanged(outPath, feed.atom1());
  564. } catch (err) {
  565. return Promise.reject(err);
  566. }
  567. } else {
  568. console.log('no articles!'); // eslint-disable-line
  569. }
  570. return Promise.resolve();
  571. }).then(function() {
  572. // this used to insert a table of contents
  573. // but it was useless being auto-generated
  574. applyTemplateToFile('build/templates/index.template', path.join(options.lessons, 'index.md'), path.join(g_outBaseDir, options.lessons, 'index.html'), {
  575. table_of_contents: '',
  576. templateOptions: g_langInfo,
  577. });
  578. return Promise.resolve();
  579. }, function(err) {
  580. error('ERROR!:');
  581. error(err);
  582. if (err.stack) {
  583. error(err.stack); // eslint-disable-line
  584. }
  585. throw new Error(err.toString());
  586. });
  587. };
  588. this.writeGlobalFiles = function() {
  589. const sm = sitemap.createSitemap({
  590. hostname: settings.baseUrl,
  591. cacheTime: 600000,
  592. });
  593. const articleLangs = { };
  594. Object.keys(g_articlesByLang).forEach(function(filename) {
  595. const article = g_articlesByLang[filename];
  596. const langs = {};
  597. article.links.forEach(function(link) {
  598. langs[link.lang] = true;
  599. });
  600. articleLangs[filename] = langs;
  601. sm.add(article);
  602. });
  603. // var langInfo = {
  604. // articles: articleLangs,
  605. // langs: g_langDB,
  606. // };
  607. // var langJS = 'window.langDB = ' + JSON.stringify(langInfo, null, 2);
  608. // writeFileIfChanged(path.join(g_outBaseDir, 'langdb.js'), langJS);
  609. writeFileIfChanged(path.join(g_outBaseDir, 'sitemap.xml'), sm.toString());
  610. copyFile(path.join(g_outBaseDir, `${settings.rootFolder}/lessons/atom.xml`), path.join(g_outBaseDir, 'atom.xml'));
  611. copyFile(path.join(g_outBaseDir, `${settings.rootFolder}/lessons/index.html`), path.join(g_outBaseDir, 'index.html'));
  612. applyTemplateToFile('build/templates/index.template', 'contributors.md', path.join(g_outBaseDir, 'contributors.html'), {
  613. table_of_contents: '',
  614. templateOptions: '',
  615. });
  616. };
  617. };
  618. const b = new Builder(settings.outDir, {
  619. origPath: `${settings.rootFolder}/lessons`, // english articles
  620. });
  621. const readdirs = function(dirpath) {
  622. const dirsOnly = function(filename) {
  623. const stat = fs.statSync(filename);
  624. return stat.isDirectory();
  625. };
  626. const addPath = function(filename) {
  627. return path.join(dirpath, filename);
  628. };
  629. return fs.readdirSync(`${settings.rootFolder}/lessons`)
  630. .map(addPath)
  631. .filter(dirsOnly);
  632. };
  633. const isLangFolder = function(dirname) {
  634. const filename = path.join(dirname, 'langinfo.hanson');
  635. return fs.existsSync(filename);
  636. };
  637. const pathToLang = function(filename) {
  638. const lang = path.basename(filename);
  639. const lessonBase = `${settings.rootFolder}/lessons`;
  640. const lessons = `${lessonBase}/${lang}`;
  641. return {
  642. lang,
  643. toc: `${settings.rootFolder}/lessons/${lang}/toc.html`,
  644. lessons: `${lessonBase}/${lang}`,
  645. template: 'build/templates/lesson.template',
  646. examplePath: `/${lessonBase}/`,
  647. home: `/${lessons}/`,
  648. };
  649. };
  650. let langs = [
  651. // English is special (sorry it's where I started)
  652. {
  653. template: 'build/templates/lesson.template',
  654. lessons: `${settings.rootFolder}/lessons`,
  655. lang: 'en',
  656. toc: `${settings.rootFolder}/lessons/toc.html`,
  657. examplePath: `/${settings.rootFolder}/lessons/`,
  658. home: '/',
  659. },
  660. ];
  661. langs = langs.concat(readdirs(`${settings.rootFolder}/lessons`)
  662. .filter(isLangFolder)
  663. .map(pathToLang));
  664. b.preProcess(langs);
  665. {
  666. const filename = path.join(settings.outDir, 'link-check.html');
  667. const html = `
  668. <html>
  669. <body>
  670. ${langs.map(lang => `<a href="${lang.home}">${lang.lang}</a>`).join('\n')}
  671. </body>
  672. </html>
  673. `;
  674. writeFileIfChanged(filename, html);
  675. }
  676. const tasks = langs.map(function(lang) {
  677. return function() {
  678. return b.process(lang);
  679. };
  680. });
  681. return tasks.reduce(function(cur, next) {
  682. return cur.then(next);
  683. }, Promise.resolve()).then(function() {
  684. b.writeGlobalFiles();
  685. cache.clear();
  686. return numErrors ? Promise.reject(new Error(`${numErrors} errors`)) : Promise.resolve();
  687. });
  688. };
粤ICP备19079148号