docswriter.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. const fs = require('fs')
  2. const path = require('path')
  3. const util = require('util')
  4. class DocsWriter {
  5. /**
  6. * Utility class used to write the HTML files used on the documentation.
  7. *
  8. * Initializes the writer to the specified output file,
  9. * creating the parent directories when used if required.
  10. */
  11. constructor(filename, typeToPath) {
  12. this.filename = filename
  13. this._parent = path.join(this.filename, '..')
  14. this.handle = null
  15. this.title = ''
  16. // Should be set before calling adding items to the menu
  17. this.menuSeparatorTag = null
  18. // Utility functions
  19. this.typeToPath = (t) => this._rel(typeToPath(t))
  20. // Control signals
  21. this.menuBegan = false
  22. this.tableColumns = 0
  23. this.tableColumnsLeft = null
  24. this.writeCopyScript = false
  25. this._script = ''
  26. }
  27. /**
  28. * Get the relative path for the given path from the current
  29. * file by working around https://bugs.python.org/issue20012.
  30. */
  31. _rel(path_) {
  32. return path
  33. .relative(this._parent, path_)
  34. .replace(new RegExp(`\\${path.sep}`, 'g'), '/')
  35. }
  36. /**
  37. * Writes the head part for the generated document,
  38. * with the given title and CSS
  39. */
  40. // High level writing
  41. writeHead(title, cssPath, defaultCss) {
  42. this.title = title
  43. this.write(
  44. `<!DOCTYPE html>
  45. <html>
  46. <head>
  47. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  48. <title>${title}</title>
  49. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  50. <link id="style" href="${this._rel(
  51. cssPath
  52. )}/docs.dark.css" rel="stylesheet">
  53. <script>
  54. document.getElementById("style").href = "${this._rel(cssPath)}/docs."
  55. + (localStorage.getItem("theme") || "${defaultCss}")
  56. + ".css";
  57. </script>
  58. <link href="https://fonts.googleapis.com/css?family=Nunito|Source+Code+Pro"
  59. rel="stylesheet">
  60. </head>
  61. <body>
  62. <div id="main_div">`
  63. )
  64. }
  65. /**
  66. * Sets the menu separator.
  67. * Must be called before adding entries to the menu
  68. */
  69. setMenuSeparator(img) {
  70. if (img) {
  71. this.menuSeparatorTag = `<img src="${this._rel(img)}" alt="/" />`
  72. } else {
  73. this.menuSeparatorTag = null
  74. }
  75. }
  76. /**
  77. * Adds a menu entry, will create it if it doesn't exist yet
  78. */
  79. addMenu(name, link) {
  80. if (this.menuBegan) {
  81. if (this.menuSeparatorTag) {
  82. this.write(this.menuSeparatorTag)
  83. }
  84. } else {
  85. // First time, create the menu tag
  86. this.write('<ul class="horizontal">')
  87. this.menuBegan = true
  88. }
  89. this.write('<li>')
  90. if (link) {
  91. this.write(`<a href="${this._rel(link)}">`)
  92. }
  93. // Write the real menu entry text
  94. this.write(name)
  95. if (link) {
  96. this.write('</a>')
  97. }
  98. this.write('</li>')
  99. }
  100. /**
  101. * Ends an opened menu
  102. */
  103. endMenu() {
  104. if (!this.menuBegan) {
  105. throw new Error('No menu had been started in the first place.')
  106. }
  107. this.write('</ul>')
  108. }
  109. /**
  110. * Writes a title header in the document body,
  111. * with an optional depth level
  112. */
  113. writeTitle(title, level, id) {
  114. level = level || 1
  115. if (id) {
  116. this.write(`<h${level} id="${id}">${title}</h${level}>`)
  117. } else {
  118. this.write(`<h${level}>${title}</h${level}>`)
  119. }
  120. }
  121. /**
  122. * Writes the code for the given 'tlobject' properly
  123. * formatted with hyperlinks
  124. */
  125. writeCode(tlobject) {
  126. this.write(
  127. `<pre>---${tlobject.isFunction ? 'functions' : 'types'}---\n`
  128. )
  129. // Write the function or type and its ID
  130. if (tlobject.namespace) {
  131. this.write(tlobject.namespace)
  132. this.write('.')
  133. }
  134. this.write(
  135. `${tlobject.name}#${tlobject.id.toString(16).padStart(8, '0')}`
  136. )
  137. // Write all the arguments (or do nothing if there's none)
  138. for (const arg of tlobject.args) {
  139. this.write(' ')
  140. const addLink = !arg.genericDefinition && !arg.isGeneric
  141. // "Opening" modifiers
  142. if (arg.genericDefinition) {
  143. this.write('{')
  144. }
  145. // Argument name
  146. this.write(arg.name)
  147. this.write(':')
  148. // "Opening" modifiers
  149. if (arg.isFlag) {
  150. this.write(`flags.${arg.flagIndex}?`)
  151. }
  152. if (arg.isGeneric) {
  153. this.write('!')
  154. }
  155. if (arg.isVector) {
  156. this.write(
  157. `<a href="${this.typeToPath('vector')}">Vector</a>&lt;`
  158. )
  159. }
  160. // Argument type
  161. if (arg.type) {
  162. if (addLink) {
  163. this.write(`<a href="${this.typeToPath(arg.type)}">`)
  164. }
  165. this.write(arg.type)
  166. if (addLink) {
  167. this.write('</a>')
  168. }
  169. } else {
  170. this.write('#')
  171. }
  172. // "Closing" modifiers
  173. if (arg.isVector) {
  174. this.write('&gt;')
  175. }
  176. if (arg.genericDefinition) {
  177. this.write('}')
  178. }
  179. }
  180. // Now write the resulting type (result from a function/type)
  181. this.write(' = ')
  182. const [genericName] = tlobject.args
  183. .filter((arg) => arg.genericDefinition)
  184. .map((arg) => arg.name)
  185. if (tlobject.result === genericName) {
  186. // Generic results cannot have any link
  187. this.write(tlobject.result)
  188. } else {
  189. if (/^vector</i.test(tlobject.result)) {
  190. // Notice that we don't simply make up the "Vector" part,
  191. // because some requests (as of now, only FutureSalts),
  192. // use a lower type name for it (see #81)
  193. let [vector, inner] = tlobject.result.split('<')
  194. inner = inner.replace(/>+$/, '')
  195. this.write(
  196. `<a href="${this.typeToPath(vector)}">${vector}</a>&lt;`
  197. )
  198. this.write(
  199. `<a href="${this.typeToPath(inner)}">${inner}</a>&gt;`
  200. )
  201. } else {
  202. this.write(
  203. `<a href="${this.typeToPath(tlobject.result)}">${
  204. tlobject.result
  205. }</a>`
  206. )
  207. }
  208. }
  209. this.write('</pre>')
  210. }
  211. /**
  212. * Begins a table with the given 'column_count', required to automatically
  213. * create the right amount of columns when adding items to the rows
  214. */
  215. beginTable(columnCount) {
  216. this.tableColumns = columnCount
  217. this.tableColumnsLeft = 0
  218. this.write('<table>')
  219. }
  220. /**
  221. * This will create a new row, or add text to the next column
  222. * of the previously created, incomplete row, closing it if complete
  223. */
  224. addRow(text, link, bold, align) {
  225. if (!this.tableColumnsLeft) {
  226. // Starting a new row
  227. this.write('<tr>')
  228. this.tableColumnsLeft = this.tableColumns
  229. }
  230. this.write('<td')
  231. if (align) {
  232. this.write(` style="text-align: ${align}"`)
  233. }
  234. this.write('>')
  235. if (bold) {
  236. this.write('<b>')
  237. }
  238. if (link) {
  239. this.write(`<a href="${this._rel(link)}">`)
  240. }
  241. // Finally write the real table data, the given text
  242. this.write(text)
  243. if (link) {
  244. this.write('</a>')
  245. }
  246. if (bold) {
  247. this.write('</b>')
  248. }
  249. this.write('</td>')
  250. this.tableColumnsLeft -= 1
  251. if (!this.tableColumnsLeft) {
  252. this.write('</tr>')
  253. }
  254. }
  255. endTable() {
  256. if (this.tableColumnsLeft) {
  257. this.write('</tr>')
  258. }
  259. this.write('</table>')
  260. }
  261. /**
  262. * Writes a paragraph of text
  263. */
  264. writeText(text) {
  265. this.write(`<p>${text}</p>`)
  266. }
  267. /**
  268. * Writes a button with 'text' which can be used
  269. * to copy 'textToCopy' to clipboard when it's clicked.
  270. */
  271. writeCopyButton(text, textToCopy) {
  272. this.writeCopyScript = true
  273. this.write(
  274. `<button onclick="cp('${textToCopy.replace(
  275. /'/g,
  276. '\\\''
  277. )}');">${text}</button>`
  278. )
  279. }
  280. addScript(src, path) {
  281. if (path) {
  282. this._script += `<script src="${this._rel(path)}"></script>`
  283. } else if (src) {
  284. this._script += `<script>${src}</script>`
  285. }
  286. }
  287. /**
  288. * Ends the whole document. This should be called the last
  289. */
  290. endBody() {
  291. if (this.writeCopyScript) {
  292. this.write(
  293. '<textarea id="c" class="invisible"></textarea>' +
  294. '<script>' +
  295. 'function cp(t){' +
  296. 'var c=document.getElementById("c");' +
  297. 'c.value=t;' +
  298. 'c.select();' +
  299. 'try{document.execCommand("copy")}' +
  300. 'catch(e){}}' +
  301. '</script>'
  302. )
  303. }
  304. this.write(`</div>${this._script}</body></html>`)
  305. }
  306. /**
  307. * Wrapper around handle.write
  308. */
  309. // "Low" level writing
  310. write(s, ...args) {
  311. if (args.length) {
  312. fs.appendFileSync(this.handle, util.format(s, ...args))
  313. } else {
  314. fs.appendFileSync(this.handle, s)
  315. }
  316. }
  317. open() {
  318. // Sanity check
  319. const parent = path.join(this.filename, '..')
  320. fs.mkdirSync(parent, { recursive: true })
  321. this.handle = fs.openSync(this.filename, 'w')
  322. return this
  323. }
  324. close() {
  325. fs.closeSync(this.handle)
  326. }
  327. }
  328. module.exports = {
  329. DocsWriter,
  330. }