gulpfile.js 11 KB

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