build.js 28 KB

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