build.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. module.exports = function () { // wrapper in case we're in module_context mode
  2. "use strict";
  3. const args = require('minimist')(process.argv.slice(2));
  4. const cache = new (require('inmemfilecache'));
  5. const Feed = require('feed');
  6. const fs = require('fs');
  7. const glob = require('glob');
  8. const Handlebars = require('handlebars');
  9. const hanson = require('hanson');
  10. const marked = require('marked');
  11. const path = require('path');
  12. const Promise = require('promise');
  13. const sitemap = require('sitemap');
  14. const utils = require('./utils');
  15. const moment = require('moment');
  16. const url = require('url');
  17. //process.title = "build";
  18. var executeP = Promise.denodeify(utils.execute);
  19. marked.setOptions({
  20. rawHtml: true,
  21. //pedantic: true,
  22. });
  23. function applyObject(src, dst) {
  24. Object.keys(src).forEach(function(key) {
  25. dst[key] = src[key];
  26. });
  27. return dst;
  28. }
  29. function mergeObjects() {
  30. var merged = {};
  31. Array.prototype.slice.call(arguments).forEach(function(src) {
  32. applyObject(src, merged);
  33. });
  34. return merged;
  35. }
  36. function readFile(fileName) {
  37. return cache.readFileSync(fileName, "utf-8");
  38. }
  39. function writeFileIfChanged(fileName, content) {
  40. if (fs.existsSync(fileName)) {
  41. var old = readFile(fileName);
  42. if (content == old) {
  43. return;
  44. }
  45. }
  46. fs.writeFileSync(fileName, content);
  47. console.log("Wrote: " + fileName);
  48. };
  49. function copyFile(src, dst) {
  50. writeFileIfChanged(dst, readFile(src));
  51. }
  52. function replaceParams(str, params) {
  53. var template = Handlebars.compile(str);
  54. if (Array.isArray(params)) {
  55. params = mergeObjects.apply(null, params.slice().reverse());
  56. }
  57. return template(params);
  58. }
  59. function encodeQuery(query) {
  60. if (!query) {
  61. return '';
  62. }
  63. return '?' + query.split("&").map(function(pair) {
  64. return pair.split("=").map(function (kv) {
  65. return encodeURIComponent(decodeURIComponent(kv));
  66. }).join('=');
  67. }).join('&');
  68. }
  69. function encodeUrl(src) {
  70. const u = url.parse(src);
  71. u.search = encodeQuery(u.query);
  72. return url.format(u);
  73. }
  74. function TemplateManager() {
  75. var templates = {};
  76. this.apply = function(filename, params) {
  77. var template = templates[filename];
  78. if (!template) {
  79. var template = Handlebars.compile(readFile(filename));
  80. templates[filename] = template;
  81. }
  82. if (Array.isArray(params)) {
  83. params = mergeObjects.apply(null, params.slice().reverse());
  84. }
  85. return template(params);
  86. };
  87. }
  88. var templateManager = new TemplateManager();
  89. Handlebars.registerHelper('include', function(filename, options) {
  90. var context;
  91. if (options && options.hash && options.hash.filename) {
  92. var varName = options.hash.filename;
  93. filename = options.data.root[varName];
  94. context = options.hash;
  95. } else {
  96. context = options.data.root;
  97. }
  98. return templateManager.apply(filename, context);
  99. });
  100. Handlebars.registerHelper('example', function(options) {
  101. options.hash.width = options.hash.width ? "width: " + options.hash.width + "px;" : "";
  102. options.hash.height = options.hash.height ? "height: " + options.hash.height + "px;" : "";
  103. options.hash.caption = options.hash.caption || options.data.root.defaultExampleCaption;
  104. options.hash.examplePath = options.data.root.examplePath;
  105. options.hash.encodedUrl = encodeURIComponent(encodeUrl(options.hash.url));
  106. options.hash.url = encodeUrl(options.hash.url);
  107. return templateManager.apply("build/templates/example.template", options.hash);
  108. });
  109. Handlebars.registerHelper('diagram', function(options) {
  110. options.hash.width = options.hash.width || "400";
  111. options.hash.height = options.hash.height || "300";
  112. options.hash.examplePath = options.data.root.examplePath;
  113. options.hash.className = options.hash.className || "";
  114. options.hash.url = encodeUrl(options.hash.url);
  115. return templateManager.apply("build/templates/diagram.template", options.hash);
  116. });
  117. Handlebars.registerHelper('image', function(options) {
  118. options.hash.examplePath = options.data.root.examplePath;
  119. options.hash.className = options.hash.className || "";
  120. options.hash.caption = options.hash.caption || "";
  121. if (options.hash.url.substring(0, 4) === 'http') {
  122. options.hash.examplePath = "";
  123. }
  124. return templateManager.apply("build/templates/image.template", options.hash);
  125. });
  126. Handlebars.registerHelper('selected', function(options) {
  127. const key = options.hash.key;
  128. const value = options.hash.value;
  129. const re = options.hash.re;
  130. const sub = options.hash.sub;
  131. let a = this[key];
  132. let b = options.data.root[value];
  133. if (re) {
  134. const r = new RegExp(re);
  135. b = b.replace(r, sub);
  136. }
  137. return a === b ? 'selected' : '';
  138. });
  139. function slashify(s) {
  140. return s.replace(/\\/g, '/');
  141. }
  142. var Builder = function(outBaseDir, options) {
  143. var g_articlesByLang = {};
  144. var g_articles = [];
  145. var g_langInfo;
  146. var g_langDB = {};
  147. var g_outBaseDir = outBaseDir;
  148. var g_origPath = options.origPath;
  149. // This are the english articles.
  150. var g_origArticles = glob.sync(path.join(g_origPath, "*.md")).map(a => path.basename(a)).filter(a => a !== 'index.md');
  151. var extractHeader = (function() {
  152. var headerRE = /([A-Z0-9_-]+): (.*?)$/i;
  153. return function(content) {
  154. var metaData = { };
  155. var lines = content.split("\n");
  156. while (true) {
  157. var line = lines[0].trim();
  158. var m = headerRE.exec(line);
  159. if (!m) {
  160. break;
  161. }
  162. metaData[m[1].toLowerCase()] = m[2];
  163. lines.shift();
  164. }
  165. return {
  166. content: lines.join("\n"),
  167. headers: metaData,
  168. };
  169. };
  170. }());
  171. var parseMD = function(content) {
  172. return extractHeader(content);
  173. };
  174. var loadMD = function(contentFileName) {
  175. var content = cache.readFileSync(contentFileName, "utf-8");
  176. return parseMD(content);
  177. };
  178. function extractHandlebars(content) {
  179. var tripleRE = /\{\{\{.*?\}\}\}/g;
  180. var doubleRE = /\{\{\{.*?\}\}\}/g;
  181. var numExtractions = 0;
  182. var extractions = {
  183. };
  184. function saveHandlebar(match) {
  185. var id = "==HANDLEBARS_ID_" + (++numExtractions) + "==";
  186. extractions[id] = match;
  187. return id;
  188. }
  189. content = content.replace(tripleRE, saveHandlebar);
  190. content = content.replace(doubleRE, saveHandlebar);
  191. return {
  192. content: content,
  193. extractions: extractions,
  194. };
  195. }
  196. function insertHandlebars(info, content) {
  197. var handlebarRE = /==HANDLEBARS_ID_\d+==/g;
  198. function restoreHandlebar(match) {
  199. var value = info.extractions[match];
  200. if (value === undefined) {
  201. throw new Error("no match restoring handlebar for: " + match);
  202. }
  203. return value;
  204. }
  205. content = content.replace(handlebarRE, restoreHandlebar);
  206. return content;
  207. }
  208. var applyTemplateToContent = function(templatePath, contentFileName, outFileName, opt_extra, data) {
  209. // Call prep's Content which parses the HTML. This helps us find missing tags
  210. // should probably call something else.
  211. //Convert(md_content)
  212. var metaData = data.headers;
  213. var content = data.content;
  214. //console.log(JSON.stringify(metaData, undefined, " "));
  215. var info = extractHandlebars(content);
  216. var html = marked(info.content);
  217. html = insertHandlebars(info, html);
  218. html = replaceParams(html, [opt_extra, g_langInfo]);
  219. const relativeOutName = slashify(outFileName).substring(g_outBaseDir.length);
  220. const langs = Object.keys(g_langDB).map((name) => {
  221. const lang = g_langDB[name];
  222. const url = slashify(path.join(lang.basePath, path.basename(outFileName)))
  223. .replace("index.html", "")
  224. .replace(/^\/threejs\/lessons\/$/, '/');
  225. return {
  226. lang: lang.lang,
  227. language: lang.language,
  228. url: url,
  229. };
  230. });
  231. metaData['content'] = html;
  232. metaData['langs'] = langs;
  233. metaData['src_file_name'] = slashify(contentFileName);
  234. metaData['dst_file_name'] = relativeOutName;
  235. metaData['basedir'] = "";
  236. metaData['toc'] = opt_extra.toc;
  237. metaData['templateOptions'] = opt_extra.templateOptions;
  238. metaData['langInfo'] = g_langInfo;
  239. metaData['url'] = "http://threejsfundamentals.org" + relativeOutName;
  240. metaData['relUrl'] = relativeOutName;
  241. metaData['screenshot'] = "http://threejsfundamentals.org/threejs/lessons/resources/threejsfundamentals.jpg";
  242. var basename = path.basename(contentFileName, ".md");
  243. [".jpg", ".png"].forEach(function(ext) {
  244. var filename = path.join("threejs", "lessons", "screenshots", basename + ext);
  245. if (fs.existsSync(filename)) {
  246. metaData['screenshot'] = "http://threejsfundamentals.org/threejs/lessons/screenshots/" + basename + ext;
  247. }
  248. });
  249. var output = templateManager.apply(templatePath, metaData);
  250. writeFileIfChanged(outFileName, output);
  251. return metaData;
  252. };
  253. var applyTemplateToFile = function(templatePath, contentFileName, outFileName, opt_extra) {
  254. console.log("processing: ", contentFileName);
  255. opt_extra = opt_extra || {};
  256. var data = loadMD(contentFileName);
  257. var metaData= applyTemplateToContent(templatePath, contentFileName, outFileName, opt_extra, data);
  258. g_articles.push(metaData);
  259. };
  260. var applyTemplateToFiles = function(templatePath, filesSpec, extra) {
  261. var files = glob.sync(filesSpec).sort();
  262. files.forEach(function(fileName) {
  263. var ext = path.extname(fileName);
  264. var baseName = fileName.substr(0, fileName.length - ext.length);
  265. var outFileName = path.join(outBaseDir, baseName + ".html");
  266. applyTemplateToFile(templatePath, fileName, outFileName, extra);
  267. });
  268. };
  269. var addArticleByLang = function(article, lang) {
  270. var filename = path.basename(article.dst_file_name);
  271. var articleInfo = g_articlesByLang[filename];
  272. var url = "http://threejsfundamentals.org" + article.dst_file_name;
  273. if (!articleInfo) {
  274. articleInfo = {
  275. url: url,
  276. changefreq: 'monthly',
  277. links: [],
  278. };
  279. g_articlesByLang[filename] = articleInfo;
  280. }
  281. articleInfo.links.push({
  282. url: url,
  283. lang: lang,
  284. });
  285. };
  286. var getLanguageSelection = function(lang) {
  287. var lessons = lang.lessons || ("threejs/lessons/" + lang.lang);
  288. var langInfo = hanson.parse(fs.readFileSync(path.join(lessons, "langinfo.hanson"), {encoding: "utf8"}));
  289. langInfo.langCode = langInfo.langCode || lang.lang;
  290. langInfo.home = lang.home || ('/' + lessons + '/');
  291. g_langDB[lang.lang] = {
  292. lang: lang.lang,
  293. language: langInfo.language,
  294. basePath: '/' + lessons,
  295. langInfo: langInfo,
  296. };
  297. };
  298. this.preProcess = function(langs) {
  299. langs.forEach(getLanguageSelection);
  300. };
  301. this.process = function(options) {
  302. console.log("Processing Lang: " + options.lang);
  303. options.lessons = options.lessons || ("threejs/lessons/" + options.lang);
  304. options.toc = options.toc || ("threejs/lessons/" + options.lang + "/toc.html");
  305. options.template = options.template || "build/templates/lesson.template";
  306. options.examplePath = options.examplePath === undefined ? "/threejs/lessons/" : options.examplePath;
  307. g_articles = [];
  308. g_langInfo = g_langDB[options.lang].langInfo;
  309. applyTemplateToFiles(options.template, path.join(options.lessons, "threejs*.md"), options);
  310. // generate place holders for non-translated files
  311. var articlesFilenames = g_articles.map(a => path.basename(a.src_file_name));
  312. var missing = g_origArticles.filter(name => articlesFilenames.indexOf(name) < 0);
  313. missing.forEach(name => {
  314. const ext = path.extname(name);
  315. const baseName = name.substr(0, name.length - ext.length);
  316. const outFileName = path.join(outBaseDir, options.lessons, baseName + ".html");
  317. const data = Object.assign({}, loadMD(path.join(g_origPath, name)));
  318. data.content = g_langInfo.missing;
  319. const extra = {
  320. origLink: '/' + slashify(path.join(g_origPath, baseName + ".html")),
  321. toc: options.toc,
  322. };
  323. console.log(" generating missing:", outFileName);
  324. applyTemplateToContent(
  325. "build/templates/missing.template",
  326. path.join(options.lessons, "langinfo.hanson"),
  327. outFileName,
  328. extra,
  329. data);
  330. });
  331. function utcMomentFromGitLog(result) {
  332. const dateStr = result.stdout.split("\n")[0].trim();
  333. let utcDateStr = dateStr
  334. .replace(/"/g, "") // WTF to these quotes come from!??!
  335. .replace(" ", "T")
  336. .replace(" ", "")
  337. .replace(/(\d\d)$/, ':$1');
  338. return moment.utc(utcDateStr);
  339. }
  340. const tasks = g_articles.map((article, ndx) => {
  341. return function() {
  342. return executeP('git', [
  343. 'log',
  344. '--format="%ci"',
  345. '--name-only',
  346. '--diff-filter=A',
  347. article.src_file_name,
  348. ]).then((result) => {
  349. article.dateAdded = utcMomentFromGitLog(result);
  350. });
  351. };
  352. }).concat(g_articles.map((article, ndx) => {
  353. return function() {
  354. return executeP('git', [
  355. 'log',
  356. '--format="%ci"',
  357. '--name-only',
  358. '--max-count=1',
  359. article.src_file_name,
  360. ]).then((result) => {
  361. article.dateModified = utcMomentFromGitLog(result);
  362. });
  363. };
  364. }));
  365. return tasks.reduce(function(cur, next){
  366. return cur.then(next);
  367. }, Promise.resolve()).then(function() {
  368. var articles = g_articles.filter(function(article) {
  369. return article.dateAdded != undefined;
  370. });
  371. articles = articles.sort(function(a, b) {
  372. return b.dateAdded - a.dateAdded;
  373. });
  374. var feed = new Feed({
  375. title: g_langInfo.title,
  376. description: g_langInfo.description,
  377. link: g_langInfo.link,
  378. image: 'http://threejsfundamentals.org/threejs/lessons/resources/threejsfundamentals.jpg',
  379. date: articles[0].dateModified.toDate(),
  380. published: articles[0].dateModified.toDate(),
  381. updated: articles[0].dateModified.toDate(),
  382. author: {
  383. name: 'threejsfundamenals contributors',
  384. link: 'http://threejsfundamentals.org/contributors.html',
  385. },
  386. });
  387. articles.forEach(function(article, ndx) {
  388. feed.addItem({
  389. title: article.title,
  390. link: "http://threejsfundamentals.org" + article.dst_file_name,
  391. description: "",
  392. author: [
  393. {
  394. name: 'threejsfundamenals contributors',
  395. link: 'http://threejsfundamentals.org/contributors.html',
  396. },
  397. ],
  398. // contributor: [
  399. // ],
  400. date: article.dateModified.toDate(),
  401. published: article.dateAdded.toDate(),
  402. // image: posts[key].image
  403. });
  404. addArticleByLang(article, options.lang);
  405. });
  406. try {
  407. const outPath = path.join(g_outBaseDir, options.lessons, "atom.xml");
  408. console.log("write:", outPath);
  409. writeFileIfChanged(outPath, feed.atom1());
  410. } catch (err) {
  411. return Promise.reject(err);
  412. }
  413. return Promise.resolve();
  414. }).then(function() {
  415. // this used to insert a table of contents
  416. // but it was useless being auto-generated
  417. applyTemplateToFile("build/templates/index.template", path.join(options.lessons, "index.md"), path.join(g_outBaseDir, options.lessons, "index.html"), {
  418. table_of_contents: "",
  419. templateOptions: g_langInfo,
  420. });
  421. return Promise.resolve();
  422. }, function(err) {
  423. console.error("ERROR!:");
  424. console.error(err);
  425. if (err.stack) {
  426. console.error(err.stack);
  427. }
  428. throw new Error(err.toString());
  429. });
  430. }
  431. this.writeGlobalFiles = function() {
  432. var sm = sitemap.createSitemap ({
  433. hostname: 'http://threejsfundamentals.org',
  434. cacheTime: 600000,
  435. });
  436. var articleLangs = { };
  437. Object.keys(g_articlesByLang).forEach(function(filename) {
  438. var article = g_articlesByLang[filename];
  439. var langs = {};
  440. article.links.forEach(function(link) {
  441. langs[link.lang] = true;
  442. });
  443. articleLangs[filename] = langs;
  444. sm.add(article);
  445. });
  446. // var langInfo = {
  447. // articles: articleLangs,
  448. // langs: g_langDB,
  449. // };
  450. // var langJS = "window.langDB = " + JSON.stringify(langInfo, null, 2);
  451. // writeFileIfChanged(path.join(g_outBaseDir, "langdb.js"), langJS);
  452. writeFileIfChanged(path.join(g_outBaseDir, "sitemap.xml"), sm.toString());
  453. copyFile(path.join(g_outBaseDir, "threejs/lessons/atom.xml"), path.join(g_outBaseDir, "atom.xml"));
  454. copyFile(path.join(g_outBaseDir, "threejs/lessons/index.html"), path.join(g_outBaseDir, "index.html"));
  455. applyTemplateToFile("build/templates/index.template", "contributors.md", path.join(g_outBaseDir, "contributors.html"), {
  456. table_of_contents: "",
  457. templateOptions: "",
  458. });
  459. };
  460. };
  461. var b = new Builder("out", {
  462. origPath: "threejs/lessons", // english articles
  463. });
  464. var readdirs = function(dirpath) {
  465. var dirsOnly = function(filename) {
  466. var stat = fs.statSync(filename);
  467. return stat.isDirectory();
  468. };
  469. var addPath = function(filename) {
  470. return path.join(dirpath, filename);
  471. };
  472. return fs.readdirSync("threejs/lessons")
  473. .map(addPath)
  474. .filter(dirsOnly);
  475. };
  476. var isLangFolder = function(dirname) {
  477. var filename = path.join(dirname, "langinfo.hanson");
  478. return fs.existsSync(filename);
  479. };
  480. var pathToLang = function(filename) {
  481. return {
  482. lang: path.basename(filename),
  483. };
  484. };
  485. var langs = [
  486. // English is special (sorry it's where I started)
  487. {
  488. template: "build/templates/lesson.template",
  489. lessons: "threejs/lessons",
  490. lang: 'en',
  491. toc: 'threejs/lessons/toc.html',
  492. examplePath: '/threejs/lessons/',
  493. home: '/',
  494. },
  495. ];
  496. langs = langs.concat(readdirs("threejs/lessons")
  497. .filter(isLangFolder)
  498. .map(pathToLang));
  499. b.preProcess(langs);
  500. var tasks = langs.map(function(lang) {
  501. return function() {
  502. return b.process(lang);
  503. };
  504. });
  505. return tasks.reduce(function(cur, next) {
  506. return cur.then(next);
  507. }, Promise.resolve()).then(function() {
  508. b.writeGlobalFiles();
  509. cache.clear();
  510. return Promise.resolve();
  511. });
  512. };
粤ICP备19079148号