build.js 25 KB

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