gendoc.lua 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. --!A cross-platform document build utility based on Lua
  2. --
  3. -- Licensed under the Apache License, Version 2.0 (the "License");
  4. -- you may not use this file except in compliance with the License.
  5. -- You may obtain a copy of the License at
  6. --
  7. -- http://www.apache.org/licenses/LICENSE-2.0
  8. --
  9. -- Unless required by applicable law or agreed to in writing, software
  10. -- distributed under the License is distributed on an "AS IS" BASIS,
  11. -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. -- See the License for the specific language governing permissions and
  13. -- limitations under the License.
  14. --
  15. -- Copyright (C) 2015-present, TBOOX Open Source Group.
  16. --
  17. -- @author charlesseizilles
  18. -- @file gendoc.lua
  19. --
  20. -- imports
  21. import("core.base.option")
  22. import("shared.md4c")
  23. function _load_apimetadata(filecontent, opt)
  24. opt = opt or {}
  25. local content = {}
  26. local apimetadata = {}
  27. local ismeta = false
  28. for idx, line in ipairs(filecontent:split("\n", {strict = true})) do
  29. if idx == 1 then
  30. local _, _, includepath = line:find("${include (.+)}")
  31. if includepath then
  32. local apientrydata = io.readfile(path.join(os.projectdir(), "doc", opt.locale, includepath))
  33. local apimetadata, content = _load_apimetadata(apientrydata, opt)
  34. return apimetadata, content, includepath
  35. elseif idx == 1 and line == "---" then
  36. ismeta = true
  37. end
  38. elseif ismeta then
  39. if line == "---" then
  40. ismeta = false
  41. else
  42. local key, value = line:match("(.+): (.+)")
  43. apimetadata[key] = value
  44. end
  45. else
  46. table.insert(content, line)
  47. end
  48. end
  49. if apimetadata.api then
  50. local api = apimetadata.api
  51. if api ~= "true" and api ~= "false" then
  52. for idx, line in ipairs(content) do
  53. if line:startswith("### ") then
  54. table.insert(content, idx + 1, "`" .. api .. "`")
  55. break
  56. end
  57. end
  58. end
  59. end
  60. if apimetadata.version then
  61. local names = {
  62. ["en-us"] = "Introduced in version",
  63. ["zh-cn"] = "被引入的版本"
  64. }
  65. local name = names[opt.locale]
  66. if name then
  67. table.insert(content, "#### " .. name .. " " .. apimetadata.version)
  68. end
  69. end
  70. if apimetadata.refer then
  71. local names = {
  72. ["en-us"] = "See also",
  73. ["zh-cn"] = "参考"
  74. }
  75. local name = names[opt.locale]
  76. if name then
  77. table.insert(content, "#### " .. name)
  78. local refers = {}
  79. for _, line in ipairs(apimetadata.refer:split(",%s+")) do
  80. table.insert(refers, "${link " .. line .. "}")
  81. end
  82. table.insert(content, table.concat(refers, ", "))
  83. end
  84. end
  85. return apimetadata, table.concat(content, "\n")
  86. end
  87. function _make_db()
  88. local db = {}
  89. local docroot = path.join(os.projectdir(), "doc")
  90. for _, pagefilepath in ipairs(os.files(path.join(os.projectdir(), "doc", "*", "pages.lua"))) do
  91. local locale = path.basename(path.directory(pagefilepath))
  92. local localizeddocroot = path.join(docroot, locale)
  93. db[locale] = io.load(path.join(localizeddocroot, "pages.lua"))
  94. db[locale].apis = {}
  95. db[locale].pages = {}
  96. for _, pagegroup in ipairs(db[locale].categories) do
  97. for _, page in ipairs(pagegroup.pages) do
  98. table.insert(db[locale].pages, page)
  99. for _, apientryfile in ipairs(os.files(path.join(localizeddocroot, page.docdir, "*.md"))) do
  100. local apientrydata = io.readfile(apientryfile)
  101. local apimetadata, _, includepath = _load_apimetadata(apientrydata, {locale = locale})
  102. if apimetadata.key and not includepath then
  103. assert(db[locale].apis[apimetadata.key] == nil, "keys must be unique (\"" .. apimetadata.key .. "\" was already inserted) (" .. apientryfile .. ")")
  104. db[locale].apis[apimetadata.key] = apimetadata
  105. db[locale].apis[apimetadata.key].page = page
  106. end
  107. end
  108. end
  109. end
  110. end
  111. return db
  112. end
  113. function _join_link(...)
  114. return table.concat(table.pack(...), "/")
  115. end
  116. function _make_anchor(db, key, locale, siteroot, page, text)
  117. assert(db and key and locale and siteroot and db[locale])
  118. if db[locale].apis[key] then
  119. text = text or db[locale].apis[key].name
  120. return '<a href="' .. _join_link(siteroot, locale, page) .. '#' .. db[locale].apis[key].key .. '" id="' .. db[locale].apis[key].key .. '">' .. text .. '</a>'
  121. else
  122. text = text or key
  123. return '<s>' .. text .. '</s>'
  124. end
  125. end
  126. function _make_link(db, key, locale, siteroot, text)
  127. assert(db and key and locale and siteroot and db[locale])
  128. if db[locale].apis[key] then
  129. text = text or db[locale].apis[key].name
  130. return '<a href="' .. _join_link(siteroot, locale, db[locale].apis[key].page.docdir .. ".html") .. '#' .. db[locale].apis[key].key .. '">' .. text .. '</a>'
  131. else
  132. text = text or key
  133. return '<s>' .. text .. '</s>'
  134. end
  135. end
  136. function _make_editlink(markdownpath, includepath, locale)
  137. local siteroot = "https://github.com/xmake-io/xmake-gendoc/edit/main/doc"
  138. if includepath then
  139. local pos = markdownpath:find(locale, 1, true)
  140. if pos then
  141. markdownpath = _join_link(markdownpath:sub(1, pos - 1), locale, includepath)
  142. end
  143. end
  144. return '<a href="' .. _join_link(siteroot, markdownpath) .. '" target="_blank">edit</a>'
  145. end
  146. function _build_language_selector(db, locale, siteroot, page)
  147. local languageSelect = [[
  148. <select name="language" onchange='changeLanguage(this, "]] .. locale .. [[");'>
  149. ]]
  150. local odrereddbkeys = table.orderkeys(db)
  151. for _, localename in ipairs(odrereddbkeys) do
  152. local dblocaleentry = db[localename]
  153. local selected = localename == locale and "selected" or ""
  154. languageSelect = languageSelect .. string.format([[ <option %s value="%s">%s %s</option>
  155. ]], selected, localename, dblocaleentry.flag, dblocaleentry.name)
  156. end
  157. languageSelect = languageSelect .. [[
  158. </select>
  159. <script>
  160. function changeLanguage(select, currentLang) {
  161. if(select.value != currentLang) {
  162. var newUrl = "%s/" + select.value + "/%s";
  163. var splitUrl = window.location.href.split('#');
  164. if (splitUrl.length > 1)
  165. newUrl = newUrl + '#' + splitUrl[splitUrl.length - 1];
  166. window.location.href = newUrl;
  167. }
  168. }
  169. </script>
  170. ]]
  171. return string.format(languageSelect, siteroot, page)
  172. end
  173. function _write_header(sitemap, siteroot, title)
  174. sitemap:write(string.format([[
  175. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
  176. <html>
  177. <head>
  178. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  179. <meta name="resource-type" content="document">
  180. <link rel="stylesheet" href="%s/prism.css" type="text/css" media="all">
  181. <link rel="stylesheet" href="%s/xmake.css" type="text/css" media="all">
  182. <title>%s</title>
  183. </head>
  184. <body>
  185. ]], siteroot, siteroot, title))
  186. end
  187. function _write_api(sitemap, db, locale, siteroot, page, apimetalist, apientrydata, markdownpath)
  188. local apimetadata, content, includepath = _load_apimetadata(apientrydata, {locale = locale})
  189. assert(apimetadata.api ~= nil, "entry api is nil value")
  190. assert(apimetadata.key ~= nil, "entry key is nil value")
  191. assert(apimetadata.name ~= nil, "entry name is nil value")
  192. table.insert(apimetalist, apimetadata)
  193. vprint("apimetadata", apimetadata)
  194. -- TODO auto generate links
  195. -- do not match is_arch before matching os.is_arch
  196. local orderedapikeys = table.orderkeys(db[locale].apis, function(lhs, rhs) return #lhs > #rhs end)
  197. local contentlines = content:split("\n", {strict = true})
  198. for idx, line in ipairs(contentlines) do
  199. if line:find("^### " .. apimetadata.name .. "$") then
  200. contentlines[idx] = "### " .. _make_anchor(db, apimetadata.key, locale, siteroot, page)
  201. else
  202. -- local lastfoundidx = 1
  203. -- for _, key in ipairs(orderedapikeys) do
  204. -- local api = db[locale].apis[key]
  205. -- local foundstart, foundend, match = line:find(api.name, lastfoundidx, true)
  206. -- if match then
  207. -- local linebegin
  208. -- line:gsub(match, _make_link(db, api.key, locale, siteroot), 1)
  209. -- end
  210. -- end
  211. end
  212. end
  213. content = table.concat(contentlines, '\n')
  214. local htmldata, errors = md4c.md2html(content)
  215. assert(htmldata, errors)
  216. -- add edit link
  217. local editlink = _make_editlink(markdownpath, includepath, locale)
  218. if editlink then
  219. htmldata = htmldata .. "\n" .. editlink
  220. end
  221. local findstart, findend
  222. repeat
  223. local anchor
  224. findstart, findend, anchor = htmldata:find("%${anchor ([^%s${%}]+)}")
  225. if findstart == nil then break end
  226. htmldata = htmldata:gsub("%${anchor [^%s${%}]+}", _make_anchor(db, anchor, locale, siteroot, page), 1)
  227. until not findstart
  228. repeat
  229. local anchor, text
  230. findstart, findend, anchor, text = htmldata:find("%${anchor ([^%s${%}]+) ([^${%}]+)}")
  231. if findstart == nil then break end
  232. htmldata = htmldata:gsub("%${anchor [^%s${%}]+ [^${%}]+}", _make_anchor(db, anchor, locale, siteroot, page, text), 1)
  233. until not findstart
  234. repeat
  235. local link
  236. findstart, findend, link = htmldata:find("%${link ([^%s${%}]+)}")
  237. if findstart == nil then break end
  238. htmldata = htmldata:gsub("%${link [^%s${%}]+}", _make_link(db, link, locale, siteroot), 1)
  239. until not findstart
  240. repeat
  241. local link, text
  242. findstart, findend, link, text = htmldata:find("%${link ([^%s${%}]+) ([^%{%}]+)}")
  243. if findstart == nil then break end
  244. htmldata = htmldata:gsub("%${link [^%s${%}]+ [^%{%}]+}", _make_link(db, link, locale, siteroot, text), 1)
  245. until not findstart
  246. sitemap:write(htmldata)
  247. end
  248. function _write_table_of_content(sitemap, db, locale, siteroot, page, apimetalist)
  249. local names = {
  250. ["en-us"] = "Interfaces",
  251. ["zh-cn"] = "接口"
  252. }
  253. local interfaces = names[locale]
  254. sitemap:write(string.format([[
  255. <div id="toc">
  256. <table>
  257. <thead>
  258. <tr><td>%s</td></tr>
  259. </thead>
  260. <tbody id="toc-body">]] .. "\n", interfaces))
  261. for _, apimetadata in ipairs(apimetalist) do
  262. if apimetadata.api ~= "false" then
  263. sitemap:write(' <tr><td><a href="' .. _join_link(siteroot, locale, page) .. '#' .. apimetadata.key .. '">' .. apimetadata.name .. "</a></td></tr>\n")
  264. end
  265. end
  266. sitemap:write([[
  267. </tbody>
  268. </table>
  269. </div>]])
  270. end
  271. function _write_footer(sitemap, siteroot, jssearcharray)
  272. sitemap:write(string.format("\n" .. [[
  273. <script src="%s/prism.js"></script>
  274. <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/index.min.js"></script>
  275. <script type="text/javascript">
  276. const documents = [
  277. ]] .. jssearcharray .. [[
  278. ]
  279. let miniSearch = new MiniSearch({
  280. fields: ['key', 'name'],
  281. storeFields: ['key', 'name', 'url'],
  282. searchOptions: {
  283. boost: { name: 2 },
  284. prefix: true,
  285. fuzzy: 0.4
  286. }
  287. })
  288. miniSearch.addAll(documents)
  289. function changeSearch(input) {
  290. var result = ""
  291. var found = miniSearch.search(input)
  292. found.forEach((e) => {
  293. result = result + "<tr><td><a href='" + e.url + "#" + e.key + "'>" + e.name + "</a></td></tr>"
  294. })
  295. document.getElementById("search-table-body").innerHTML = result
  296. }
  297. </script>
  298. <script type="text/javascript">
  299. function locationHashChanged(e) {
  300. var tocbody = document.getElementById("toc-body")
  301. if (tocbody) {
  302. var tocLinks = tocbody.getElementsByTagName("a")
  303. for (let i = 0;i < tocLinks.length; i++) {
  304. if (tocLinks[i].href == window.location.href) {
  305. tocLinks[i].style = "font-weight:bold"
  306. tocLinks[i].parentElement.style = "background-color:#d6ffed"
  307. } else {
  308. tocLinks[i].style = ""
  309. tocLinks[i].parentElement.style = ""
  310. }
  311. }
  312. }
  313. var navLinks = document.getElementById("sidebar-nav").getElementsByTagName("a")
  314. for (let i = 0;i < navLinks.length; i++) {
  315. const urlbase = window.location.href.split('#')
  316. if (navLinks[i].href == urlbase[0] || navLinks[i].href == window.location.href) {
  317. navLinks[i].style = "font-weight:bold"
  318. navLinks[i].parentElement.style = "background-color:#d6ffed"
  319. } else {
  320. navLinks[i].style = ""
  321. navLinks[i].parentElement.style = ""
  322. }
  323. }
  324. }
  325. window.onhashchange = locationHashChanged;
  326. locationHashChanged({})
  327. </script>
  328. </body>
  329. </html>
  330. ]], siteroot))
  331. end
  332. function _build_html_page(docdir, title, db, sidebar, jssearcharray, opt)
  333. opt = opt or {}
  334. local locale = opt.locale or "en-us"
  335. local page = docdir .. ".html"
  336. local isindex = false
  337. if title == "index" and docdir == "." then
  338. page = "index.html"
  339. isindex = true
  340. end
  341. local outputfiledir = path.join(opt.outputdir or "", locale)
  342. local outputfile = path.join(outputfiledir, page)
  343. local sitemap = io.open(outputfile, 'w')
  344. local siteroot = opt.siteroot:gsub("\\", "/")
  345. _write_header(sitemap, siteroot, title)
  346. sitemap:write('<div id="sidebar">\n')
  347. sitemap:write([[
  348. <input type="search" id="search-input" placeholder="search" name="search" oninput="changeSearch(this.value);">
  349. <table><tbody id="search-table-body"></tbody></table>
  350. ]])
  351. sitemap:write(_build_language_selector(db, locale, siteroot, page))
  352. sitemap:write(sidebar)
  353. sitemap:write('</div>\n')
  354. sitemap:write('<div id="content">\n')
  355. local isfirst = true
  356. local apimetalist = {}
  357. local docroot = path.join(os.projectdir(), "doc")
  358. local localeroot = path.join(docroot, locale)
  359. local files = {}
  360. for _, file in ipairs(os.files(path.join(localeroot, docdir, "*.md"))) do
  361. local filename = path.filename(file)
  362. if not filename:startswith("_") then
  363. if filename == "0_intro.md" then
  364. table.insert(files, 1, file)
  365. else
  366. table.insert(files, file)
  367. end
  368. end
  369. end
  370. table.sort(files)
  371. for _, file in ipairs(files) do
  372. if isfirst then
  373. isfirst = false
  374. else
  375. sitemap:write("<hr />")
  376. end
  377. vprint("loading " .. file)
  378. local apientrydata = io.readfile(file)
  379. local markdownpath = path.relative(file, docroot)
  380. _write_api(sitemap, db, locale, siteroot, page, apimetalist, apientrydata, markdownpath)
  381. end
  382. sitemap:write("</div>\n")
  383. if not isindex then
  384. _write_table_of_content(sitemap, db, locale, siteroot, page, apimetalist)
  385. end
  386. _write_footer(sitemap, siteroot, jssearcharray)
  387. sitemap:close()
  388. end
  389. function _make_search_array(db, opt)
  390. local jssearcharray = ""
  391. local odreredapikeys = table.orderkeys(db[opt.locale].apis)
  392. local id = 1
  393. for _, apikey in ipairs(odreredapikeys) do
  394. local api = db[opt.locale].apis[apikey]
  395. if api.api ~= "false" then
  396. jssearcharray = jssearcharray .. " {id: " .. tostring(id) .. ", key: \"" .. api.key .. "\", name: \"" .. api.name .. "\", url: \"" .. _join_link(opt.siteroot, opt.locale, api.page.docdir) .. ".html\" },\n"
  397. id = id + 1
  398. end
  399. end
  400. return jssearcharray:gsub("\\", "/")
  401. end
  402. function _build_html_pages(opt)
  403. opt = opt or {}
  404. os.tryrm(opt.outputdir)
  405. local db = _make_db()
  406. for _, pagefile in ipairs(os.files(path.join(os.projectdir(), "doc", "*", "pages.lua"))) do
  407. opt.locale = path.basename(path.directory(pagefile))
  408. local jssearcharray = _make_search_array(db, opt)
  409. local sidebar = '<div id="sidebar-nav">'
  410. for _, category in ipairs(db[opt.locale].categories) do
  411. sidebar = sidebar .. "\n<p>" .. category.title .. "</p>\n<ul>\n"
  412. for _, page in ipairs(category.pages) do
  413. local pagepath = page.docdir
  414. if pagepath == "." then
  415. pagepath = page.title
  416. end
  417. sidebar = sidebar .. '<li><a href="' .. _join_link(opt.siteroot, opt.locale, pagepath .. ".html") .. '">' .. page.title .. "</a></li>\n"
  418. end
  419. sidebar = sidebar .. "</ul>\n"
  420. end
  421. sidebar = sidebar .. "</div>\n"
  422. for _, page in ipairs(db[opt.locale].pages) do
  423. _build_html_page(page.docdir, page.title, db, sidebar, jssearcharray, opt)
  424. end
  425. end
  426. for _, htmlfile in ipairs(os.files(path.join(os.projectdir(), "doc", "*.html"))) do
  427. os.trycp(htmlfile, opt.outputdir)
  428. io.gsub(path.join(opt.outputdir, path.filename(htmlfile)), "%${siteroot}", opt.siteroot)
  429. end
  430. os.trycp(path.join(os.projectdir(), "resources", "*"), opt.outputdir)
  431. end
  432. function main()
  433. local outputdir = path.absolute(option.get("outputdir"))
  434. local siteroot = option.get("siteroot")
  435. if not siteroot:startswith("http") then
  436. siteroot = "file://" .. path.absolute(siteroot)
  437. end
  438. if #siteroot > 1 and siteroot:endswith("/") then
  439. siteroot = siteroot:sub(1, -2)
  440. end
  441. _build_html_pages({outputdir = outputdir, siteroot = siteroot})
  442. cprint("Generated document: ${bright}%s${clear}", outputdir)
  443. cprint("Siteroot: ${bright}%s${clear}", siteroot)
  444. end