aboutsummaryrefslogtreecommitdiff
path: root/tools/make_spec.lua
blob: acfdb612ee3395d241a2d7ea4c526923dac6e945 (plain)
  1. local lcmark = require('lcmark')
  2. local cmark = require('cmark')
  3. local format = arg[1] or 'html'
  4. local trim = function(s)
  5. return s:gsub("^%s+",""):gsub("%s+$","")
  6. end
  7. local warn = function(s)
  8. io.stderr:write('WARNING: ' .. s .. '\n')
  9. end
  10. local to_identifier = function(s)
  11. return trim(s):lower():gsub('[^%w]+', ' '):gsub('[%s]+', '-')
  12. end
  13. local render_number = function(tbl)
  14. local buf = {}
  15. for i,x in ipairs(tbl) do
  16. buf[i] = tostring(x)
  17. end
  18. return table.concat(buf, '.')
  19. end
  20. local extract_references = function(doc)
  21. local cur, entering, node_type
  22. local refs = {}
  23. for cur, entering, node_type in cmark.walk(doc) do
  24. if not entering and
  25. ((node_type == cmark.NODE_LINK and cmark.node_get_url(cur) == '@') or
  26. node_type == cmark.NODE_HEADING) then
  27. local child = cmark.node_first_child(cur)
  28. local label = trim(cmark.render_commonmark(child, OPT_DEFAULT, 0))
  29. local ident = to_identifier(label)
  30. if refs[label] then
  31. warn("duplicate reference " .. label)
  32. end
  33. refs[label] = ident
  34. if not refs[label .. 's'] then
  35. -- plural too
  36. refs[label .. 's'] = ident
  37. end
  38. end
  39. end
  40. -- check for duplicate IDs
  41. local idents = {}
  42. for _,id in ipairs(refs) do
  43. if idents[id] then
  44. warn("duplicate identifier " .. id)
  45. end
  46. idents[#idents + 1] = id
  47. end
  48. return refs
  49. end
  50. local make_toc = function(toc)
  51. -- we create a commonmark string, then parse it
  52. local toclines = {}
  53. for _,entry in ipairs(toc) do
  54. if entry.level <= 2 then
  55. local indent = string.rep(' ', entry.level - 1)
  56. toclines[#toclines + 1] = indent .. '* [' ..
  57. (entry.number == '' and ''
  58. or '<span class="number">' .. entry.number .. '</span>') ..
  59. entry.label .. '](#' .. entry.ident .. ')'
  60. end
  61. end
  62. -- now parse our cm list and return the resulting list node:
  63. local doc = cmark.parse_string(table.concat(toclines, '\n'), cmark.OPT_SMART)
  64. return cmark.node_first_child(doc)
  65. end
  66. local make_html_element = function(block, tagname, attrs)
  67. local div = cmark.node_new(block and cmark.NODE_CUSTOM_BLOCK or
  68. cmark.NODE_CUSTOM_INLINE)
  69. local attribs = {}
  70. for _,attr in ipairs(attrs) do
  71. attribs[#attribs + 1] = ' ' .. attr[1] .. '="' .. attr[2] .. '"'
  72. end
  73. local opentag = '<' .. tagname .. table.concat(attribs, '') .. '>'
  74. local closetag = '</' .. tagname .. '>'
  75. cmark.node_set_on_enter(div, opentag)
  76. cmark.node_set_on_exit(div, closetag)
  77. return div
  78. end
  79. local make_html_block = function(tagname, attrs)
  80. return make_html_element(true, tagname, attrs)
  81. end
  82. local make_html_inline = function(tagname, attrs)
  83. return make_html_element(false, tagname, attrs)
  84. end
  85. local make_latex = function(spec)
  86. local latex = cmark.node_new(spec.block and cmark.NODE_CUSTOM_BLOCK or
  87. cmark.NODE_CUSTOM_INLINE)
  88. cmark.node_set_on_enter(latex, spec.start)
  89. cmark.node_set_on_exit(latex, spec.stop)
  90. return latex
  91. end
  92. local make_text = function(s)
  93. local text = cmark.node_new(cmark.NODE_TEXT)
  94. cmark.node_set_literal(text, s)
  95. return text
  96. end
  97. local create_anchors = function(doc, meta, to)
  98. local cur, entering, node_type
  99. local toc = {}
  100. local number = {0}
  101. local example = 0
  102. for cur, entering, node_type in cmark.walk(doc) do
  103. if not entering and
  104. ((node_type == cmark.NODE_LINK and cmark.node_get_url(cur) == '@') or
  105. node_type == cmark.NODE_HEADING) then
  106. local anchor
  107. local child = cmark.node_first_child(cur)
  108. local label = trim(cmark.render_commonmark(child, OPT_DEFAULT, 0))
  109. local ident = to_identifier(label)
  110. if node_type == cmark.NODE_LINK then
  111. if format == 'latex' then
  112. anchor = make_latex({start="\\hypertarget{" .. ident .. "}{",
  113. stop="\\label{" .. ident .. "}}",
  114. block = true})
  115. else
  116. anchor = make_html_inline('a', {{'id', ident}, {'href', '#'..ident},
  117. {'class', 'definition'}})
  118. end
  119. else -- NODE_HEADING
  120. local level = cmark.node_get_heading_level(cur)
  121. local last_level = #toc == 0 and 1 or toc[#toc].level
  122. if #number > 0 then
  123. if level > last_level then -- subhead
  124. number[level] = 1
  125. else
  126. while last_level > level do
  127. number[last_level] = nil
  128. last_level = last_level - 1
  129. end
  130. number[level] = number[level] + 1
  131. end
  132. end
  133. table.insert(toc, { label = label, ident = ident, level = level, number = render_number(number) })
  134. local num = render_number(number)
  135. local section_cmds = {"\\section", "\\subsection",
  136. "\\subsubsection", "\\chapter"}
  137. if format == 'latex' then
  138. anchor = make_latex({start="\\hypertarget{" .. ident .. "}{" ..
  139. section_cmds[level] .. "{",
  140. stop="}\\label{" .. ident .. "}}",
  141. block = true})
  142. else
  143. anchor = make_html_block('h' .. tostring(level),
  144. {{'id', ident},
  145. {'href', '#'..ident},
  146. {'class', 'definition'}})
  147. if num ~= '' then
  148. local numspan = make_html_inline('span', {{'class','number'}})
  149. node_append_child(numspan, make_text(num))
  150. node_append_child(anchor, numspan)
  151. end
  152. end
  153. end
  154. local children = {};
  155. while child do
  156. children[#children + 1] = child
  157. child = cmark.node_next(child)
  158. end
  159. for _,child in ipairs(children) do
  160. node_append_child(anchor, child)
  161. end
  162. cmark.node_insert_before(cur, anchor)
  163. cmark.node_unlink(cur)
  164. elseif entering and node_type == cmark.NODE_CODE_BLOCK and
  165. cmark.node_get_fence_info(cur) == 'example' then
  166. example = example + 1
  167. -- split into two code blocks
  168. local code = cmark.node_get_literal(cur)
  169. local sepstart, sepend = code:find("[\n\r]+%.[\n\r]+")
  170. if not sepstart then
  171. warn("Could not find separator in:\n" .. contents)
  172. end
  173. local markdown_code = cmark.node_new(cmark.NODE_CODE_BLOCK)
  174. local html_code = cmark.node_new(cmark.NODE_CODE_BLOCK)
  175. -- note: we replace the ␣ with a special span after rendering
  176. local markdown_code_string = code:sub(1, sepstart):gsub(' ', '␣')
  177. local html_code_string = code:sub(sepend + 1):gsub(' ', '␣')
  178. cmark.node_set_literal(markdown_code, markdown_code_string)
  179. cmark.node_set_fence_info(markdown_code, 'markdown')
  180. cmark.node_set_literal(html_code, html_code_string)
  181. cmark.node_set_fence_info(html_code, 'html')
  182. local example_div, leftcol_div, rightcol_div
  183. if format == 'latex' then
  184. example_div = make_latex({start = '\\begin{minipage}[t]{\\textwidth}\n{\\scriptsize Example ' .. tostring(example) .. '}\n\n\\vspace{-0.4em}\n', stop = '\\end{minipage}', block = true})
  185. leftcol_div = make_latex({start = "\\begin{minipage}[t]{0.49\\textwidth}\n\\definecolor{shadecolor}{gray}{0.85}\n\\begin{snugshade}\\small\n", stop = "\\end{snugshade}\n\\end{minipage}\n\\hfill", block = true})
  186. rightcol_div = make_latex({start = "\\begin{minipage}[t]{0.49\\textwidth}\n\\definecolor{shadecolor}{gray}{0.95}\n\\begin{snugshade}\\small\n", stop = "\\end{snugshade}\n\\end{minipage}\n\\vspace{0.8em}", block = true})
  187. cmark.node_append_child(leftcol_div, markdown_code)
  188. cmark.node_append_child(rightcol_div, html_code)
  189. cmark.node_append_child(example_div, leftcol_div)
  190. cmark.node_append_child(example_div, rightcol_div)
  191. else
  192. leftcol_div = make_html_block('div', {{'class','column'}})
  193. rightcol_div = make_html_block('div', {{'class', 'column'}})
  194. cmark.node_append_child(leftcol_div, markdown_code)
  195. cmark.node_append_child(rightcol_div, html_code)
  196. local examplenum_div = make_html_block('div', {{'class', 'examplenum'}})
  197. local interact_link = make_html_inline('a', {{'class', 'dingus'},
  198. {'title', 'open in interactive dingus'}})
  199. cmark.node_append_child(interact_link, make_text("Try It"))
  200. local examplenum_link = cmark.node_new(cmark.NODE_LINK)
  201. cmark.node_set_url(examplenum_link, '#example-' .. tostring(example))
  202. cmark.node_append_child(examplenum_link,
  203. make_text("Example " .. tostring(example)))
  204. cmark.node_append_child(examplenum_div, examplenum_link)
  205. if format == 'html' then
  206. cmark.node_append_child(examplenum_div, interact_link)
  207. end
  208. example_div = make_html_block('div', {{'class', 'example'},
  209. {'id','example-' .. tostring(example)}})
  210. cmark.node_append_child(example_div, examplenum_div)
  211. cmark.node_append_child(example_div, leftcol_div)
  212. cmark.node_append_child(example_div, rightcol_div)
  213. end
  214. cmark.node_insert_before(cur, example_div)
  215. cmark.node_unlink(cur)
  216. cmark.node_free(cur)
  217. elseif node_type == cmark.NODE_HTML_BLOCK and
  218. cmark.node_get_literal(cur) == '<!-- END TESTS -->\n' then
  219. -- change numbering
  220. number = {}
  221. if format ~= 'latex' then
  222. local appendices = make_html_block('div', {{'class','appendices'}})
  223. cmark.node_insert_after(cur, appendices)
  224. -- put the remaining sections in an appendix
  225. local tmp = cmark.node_next(appendices)
  226. while tmp do
  227. cmark.node_append_child(appendices, tmp)
  228. tmp = cmark.node_next(tmp)
  229. end
  230. end
  231. end
  232. end
  233. meta.toc = make_toc(toc)
  234. end
  235. local to_ref = function(ref)
  236. return '[' .. ref.label .. ']: #' .. ref.indent .. '\n'
  237. end
  238. local inp = io.read("*a")
  239. local doc1 = cmark.parse_string(inp, cmark.OPT_DEFAULT)
  240. local refs = extract_references(doc1)
  241. local refblock = '\n'
  242. for lab,ident in pairs(refs) do
  243. refblock = refblock .. '[' .. lab .. ']: #' .. ident .. '\n'
  244. -- refblock = refblock .. '[' .. lab .. 's]: #' .. ident .. '\n'
  245. end
  246. -- append references and parse again
  247. local contents, meta, msg = lcmark.convert(inp .. refblock, format,
  248. { smart = true,
  249. yaml_metadata = true,
  250. safe = false,
  251. filters = { create_anchors }
  252. })
  253. if contents then
  254. local f = io.open("tools/template." .. format, 'r')
  255. if not f then
  256. io.stderr:write("Could not find template!")
  257. os.exit(1)
  258. end
  259. local template = f:read("*a")
  260. if format == 'html' then
  261. contents = contents:gsub('␣', '<span class="space"> </span>')
  262. end
  263. meta.body = contents
  264. local rendered, msg = lcmark.render_template(template, meta)
  265. if not rendered then
  266. io.stderr:write(msg)
  267. os.exit(1)
  268. end
  269. io.write(rendered)
  270. os.exit(0)
  271. else
  272. io.stderr:write(msg)
  273. os.exit(1)
  274. end