gulpfile.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. // Node modules
  2. var gulp = require('gulp');
  3. var watch = require('gulp-watch');
  4. var plumber = require('gulp-plumber');
  5. var preservetime = require('gulp-preservetime');
  6. var gutil = require('gulp-util');
  7. var tap = require('gulp-tap');
  8. var jsonlint = require('gulp-jsonlint');
  9. var hljs = require('highlight.js')
  10. var server = require('gulp-server-livereload');
  11. var sass = require('gulp-sass');
  12. var minify = require('gulp-cssnano');
  13. var del = require('del');
  14. var path = require('path');
  15. var mkdirp = require('mkdirp');
  16. var slugify = require('slugify');
  17. var through = require('through2');
  18. var File = require('vinyl');
  19. var hljs = require('highlight.js');
  20. var print = require('gulp-print');
  21. var frontmatter = require('front-matter');
  22. var markdown = require('markdown-it');
  23. var md_attrs = require('markdown-it-attrs');
  24. var md_container = require('markdown-it-container');
  25. var md_deflist = require('markdown-it-deflist')
  26. var md_sub = require('markdown-it-sub');
  27. var md_sup = require('markdown-it-sup');
  28. var md_katex = require('markdown-it-katex');
  29. var hercule = require('hercule');
  30. var exec = require('child_process').exec;
  31. // hljs lua highlight patched
  32. var lua = require('./lib/lua');
  33. hljs.registerLanguage('lua', lua.lua);
  34. md = new markdown({
  35. html: true,
  36. xhtmlOut: true,
  37. breaks: false,
  38. langPrefix: 'language-',
  39. linkify: true,
  40. typographer: true,
  41. highlight: function (str, lang) {
  42. if (lang && hljs.getLanguage(lang)) {
  43. try {
  44. var hl = hljs.highlight(lang, str).value;
  45. // Callouts hack!
  46. // replaces "-- [1]", "// [1]" and "-- <1>" and "// <1>" with a span
  47. var exp = /(?:--|\/\/|#) (?:\[|&lt;)([0-9]+)(?:\]|&gt;)/g;
  48. return hl.replace(exp, '<span class="callout" data-pseudo-content="$1"></span>');
  49. } catch (__) {}
  50. }
  51. return ''; // use external default escaping
  52. }});
  53. md.use(md_deflist);
  54. md.use(md_attrs);
  55. md.use(md_sub);
  56. md.use(md_sup);
  57. md.use(md_katex);
  58. md.use(md_container, 'sidenote', { render: rendernote });
  59. md.use(md_container, 'important', { render: rendernote });
  60. // Notes are rendered as two divs so they can be styled right
  61. function rendernote(tokens, idx) {
  62. if (tokens[idx].nesting === 1) {
  63. // opening tag
  64. var type = tokens[idx].info.trim().match(/^(\w+).*$/)[1];
  65. return '<div class="note ' + type + '"><div class="note-icon"></div><div class="note-content">';
  66. } else {
  67. // closing tag
  68. return '</div></div>\n';
  69. }
  70. };
  71. function slugname(str) {
  72. return '_' + slugify(str, '_').toLowerCase();
  73. }
  74. // Add anchors to all headings.
  75. md.renderer.rules.heading_open = function (tokens, idx, options, env, self) {
  76. var tag = tokens[idx].tag;
  77. var title = tokens[idx + 1].content
  78. var slug = slugname(title);
  79. // Add TOC entry
  80. if(!("toc" in env) ) {
  81. env.toc = [];
  82. }
  83. var level = tag.match(/^h(\d+)$/)[1];
  84. env.toc.push({ entry: title, slug: slug, level: level });
  85. return '<div id="' + slug + '" class="anchor"></div>' +
  86. '<' + tag + '>';
  87. };
  88. md.renderer.rules.heading_close = function (tokens, idx, options, env, self) {
  89. var slug = slugname(tokens[idx - 1].content);
  90. var tag = tokens[idx].tag;
  91. if (tag == 'h2' || tag == 'h3')
  92. return '<a href="#' + slug + '"><span class="anchor-link"></span></a>\n'
  93. + '</' + tag + '>';
  94. else
  95. return '</' + tag + '>';
  96. };
  97. // Images.
  98. md.renderer.rules.image = function (tokens, idx, options, env, self) {
  99. var token = tokens[idx];
  100. if('imgurl' in env) {
  101. // Rewrite src and srcset
  102. var src = token.attrs[token.attrIndex('src')][1];
  103. token.attrs[token.attrIndex('src')][1] = env.imgurl + '/' + src;
  104. if(token.attrs[token.attrIndex('srcset')]) {
  105. var srcset = token.attrs[token.attrIndex('srcset')][1];
  106. token.attrs[token.attrIndex('srcset')][1] = env.imgurl + '/' + srcset;
  107. }
  108. }
  109. // Set alt attribute
  110. token.attrs[token.attrIndex('alt')][1] = self.renderInlineAsText(token.children, options, env);
  111. return self.renderToken(tokens, idx, options);
  112. };
  113. var HTML_ESCAPE_TEST_RE = /[&<>"]/;
  114. var HTML_ESCAPE_REPLACE_RE = /[&<>"]/g;
  115. var HTML_REPLACEMENTS = {
  116. '&': '&amp;',
  117. '<': '&lt;',
  118. '>': '&gt;',
  119. '"': '&quot;'
  120. };
  121. function replaceUnsafeChar(ch) {
  122. return HTML_REPLACEMENTS[ch];
  123. }
  124. function escapeHtml(str) {
  125. if (HTML_ESCAPE_TEST_RE.test(str)) {
  126. return str.replace(HTML_ESCAPE_REPLACE_RE, replaceUnsafeChar);
  127. }
  128. return str;
  129. }
  130. // Fence code blocks
  131. md.renderer.rules.fence = function (tokens, idx, options, env, self) {
  132. var token = tokens[idx],
  133. info = token.info ? decodeURI(token.info).trim() : '',
  134. langName = '',
  135. highlighted, i, tmpAttrs, tmpToken;
  136. if (info) {
  137. langName = info.split(/\s+/g)[0];
  138. }
  139. if (options.highlight) {
  140. highlighted = options.highlight(token.content, langName) || escapeHtml(token.content);
  141. } else {
  142. highlighted = escapeHtml(token.content);
  143. }
  144. if (highlighted.indexOf('<pre') === 0) {
  145. return highlighted + '\n';
  146. }
  147. var id = 'codesnippet_' + idx;
  148. var copy = '<button class="copy-to-clipboard" data-target="#' + id + '"><span class="icon-clipboard"></span></button>';
  149. // If language exists, inject class gently, without modifying original token.
  150. // May be, one day we will add .clone() for token and simplify this part, but
  151. // now we prefer to keep things local.
  152. if (info) {
  153. i = token.attrIndex('class');
  154. tmpAttrs = token.attrs ? token.attrs.slice() : [];
  155. if (i < 0) {
  156. tmpAttrs.push([ 'class', options.langPrefix + langName ]);
  157. } else {
  158. tmpAttrs[i][1] += ' ' + options.langPrefix + langName;
  159. }
  160. // Fake token just to render attributes
  161. tmpToken = {
  162. attrs: tmpAttrs
  163. };
  164. return '<pre>' + '<code id="' + id +'"' + self.renderAttrs(tmpToken) + '>'
  165. + highlighted
  166. + '</code>' + copy + '</pre>\n';
  167. }
  168. return '<pre>' + '<code id="' + id +'"' + self.renderAttrs(token) + '>'
  169. + highlighted
  170. + '</code>' + copy + '</pre>\n';
  171. };
  172. // Output preview html documents
  173. function markdownToPreviewHtml(file) {
  174. var data = frontmatter(file.contents.toString());
  175. // Inject some styling html for the preview. The built htmls are clean.
  176. var head = '<!DOCTYPE html><html><head><link type="text/css" rel="stylesheet" href="/preview-md.css">' +
  177. '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.css" integrity="sha384-wITovz90syo1dJWVh32uuETPVEtGigN07tkttEqPv+uR2SE/mbQcG7ATL28aI9H0" crossorigin="anonymous">' +
  178. '</head><body>\n';
  179. head += '<div class="documentation">';
  180. var foot = '</div></body></html>\n';
  181. var html = head + md.render(data.body) + foot;
  182. file.contents = new Buffer(html);
  183. file.path = gutil.replaceExtension(file.path, '.html');
  184. return;
  185. }
  186. var img_url = 'https://storage.googleapis.com/defold-doc';
  187. //var img_url = '/_ah/gcs/defold-doc'; // local dev-server
  188. // Build document json for storage
  189. function markdownToJson(file) {
  190. var name = path.relative(file.base, file.path);
  191. // Needs language for static image url:s
  192. var m = name.match(/^(\w+)[/](\w+)[/].*$/);
  193. var lang = m[1];
  194. var doctype = m[2];
  195. var data = frontmatter(file.contents.toString());
  196. var env = { imgurl: img_url + '/' + lang + '/' + doctype };
  197. data.html = md.render(data.body, env);
  198. data.toc = env.toc;
  199. file.contents = new Buffer(JSON.stringify(data));
  200. file.path = gutil.replaceExtension(file.path, '.json');
  201. return;
  202. }
  203. // Create a map path -> [lang1, lang2 ...] and add it to the
  204. // languages.json file
  205. function langMap(jsonfile) {
  206. var langmap = require('./docs/' + jsonfile);
  207. langmap['filemap'] = {};
  208. return through.obj(function (file, enc, cb) {
  209. var fullpath = path.relative(file.base, file.path);
  210. var m = fullpath.match(/^(\w+)[/](\w+)[/].*$/);
  211. var lang = m[1];
  212. var name = path.relative(lang, fullpath);
  213. if(!langmap['filemap'][name]) {
  214. langmap['filemap'][name] = [];
  215. }
  216. langmap['filemap'][name].push(lang);
  217. cb(null, file);
  218. }, function(cb) {
  219. f = new File({
  220. path: jsonfile,
  221. contents: new Buffer(JSON.stringify(langmap))
  222. });
  223. this.push(f);
  224. cb();
  225. });
  226. }
  227. // Support for transclusion via :[](file.md) syntax
  228. function gulpHercule() {
  229. return through.obj( function(file, encoding, callback) {
  230. if (file.isNull()) {
  231. return callback(null, file);
  232. }
  233. if (file.isBuffer()) {
  234. var options = { 'source': file.path }
  235. hercule.transcludeString(file.contents.toString(encoding), options, function(err, output) {
  236. if (err) {
  237. // Handle exceptions like dead links
  238. process.stderr.write('ERROR: ' + err.message + ' (' + err.path + ')\n');
  239. process.exit(1);
  240. }
  241. file.contents = new Buffer(output);
  242. return callback(null, file);
  243. })
  244. }
  245. if (file.isStream()) {
  246. var transcluder = new hercule.TranscludeStream(options);
  247. transcluder.on('error', (err) => {
  248. // Handle exceptions like dead links
  249. process.stderr.write('ERROR: ' + err.message + ' (' + err.path + ')\n');
  250. process.exit(1);
  251. });
  252. file.contents = file.contents.pipe(transcluder);
  253. return callback(null, file);
  254. }
  255. });
  256. };
  257. // Build docs
  258. gulp.task('build', ['assets'], function () {
  259. gulp.src('docs/**/*.md')
  260. .pipe(gulpHercule())
  261. .pipe(tap(markdownToJson))
  262. .pipe(langMap('languages.json'))
  263. .pipe(gulp.dest("build"))
  264. .pipe(preservetime());;
  265. // jsonfiles directly in lang folders are verified
  266. return gulp.src(['docs/*/*.json'])
  267. .pipe(jsonlint())
  268. .pipe(jsonlint.reporter())
  269. .pipe(gulp.dest("build"))
  270. .pipe(preservetime());;
  271. });
  272. gulp.task('assets', ['clean'], function() {
  273. gulp.src(['docs/assets/**/*.*'])
  274. .pipe(gulp.dest("build/assets"))
  275. .pipe(preservetime());
  276. return gulp.src(['docs/**/*.{png,jpg,svg,gif,js,zip,js}'])
  277. .pipe(gulp.dest("build"))
  278. .pipe(preservetime());
  279. });
  280. // Watch for changes in md files and compile new html
  281. gulp.task('watch', function () {
  282. mkdirp('build/preview');
  283. gulp.src('build/preview')
  284. .pipe(server({
  285. livereload: true,
  286. open: true,
  287. directoryListing: {
  288. enable: true,
  289. path: 'build/preview'
  290. }
  291. }));
  292. watch(['docs/**/*.sass'], function () {
  293. gulp.start('sass');
  294. });
  295. gulp.start('sass');
  296. gulp.src('docs/**/images/**/*.*')
  297. .pipe(watch('docs/**/images/**/*.*'))
  298. .pipe(gulp.dest("build/preview"));
  299. return gulp.src('docs/**/*.md')
  300. .pipe(watch('docs/**/*.md'))
  301. .pipe(gulpHercule())
  302. .pipe(tap(markdownToPreviewHtml))
  303. .pipe(print())
  304. .pipe(gulp.dest("build/preview"));
  305. });
  306. gulp.task('clean', [], function() {
  307. return del(['build']);
  308. });
  309. gulp.task('sass', [], function() {
  310. gulp.src('docs/sass/preview-md.sass')
  311. .pipe(plumber())
  312. .pipe(sass())
  313. .pipe(minify())
  314. .pipe(gulp.dest('build/preview'))
  315. });