aboutsummaryrefslogtreecommitdiff
path: root/tools/make_spec.lua
blob: 3425f35ac812fe3fb19d607238b35478bfd8719c (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 children = cmark.node_first_child(cur)
  28. local label = trim(cmark.render_commonmark(children, 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 children = cmark.node_first_child(cur)
  108. local label = trim(cmark.render_commonmark(children, 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. while children do
  155. node_append_child(anchor, children)
  156. children = cmark.node_next(children)
  157. end
  158. cmark.node_insert_before(cur, anchor)
  159. cmark.node_unlink(cur)
  160. elseif entering and node_type == cmark.NODE_CODE_BLOCK and
  161. cmark.node_get_fence_info(cur) == 'example' then
  162. example = example + 1
  163. -- split into two code blocks
  164. local code = cmark.node_get_literal(cur)
  165. local sepstart, sepend = code:find("[\n\r]+%.[\n\r]+")
  166. if not sepstart then
  167. warn("Could not find separator in:\n" .. contents)
  168. end
  169. local markdown_code = cmark.node_new(cmark.NODE_CODE_BLOCK)
  170. local html_code = cmark.node_new(cmark.NODE_CODE_BLOCK)
  171. -- note: we replace the ␣ with a special span after rendering
  172. local markdown_code_string = code:sub(1, sepstart):gsub(' ', '␣')
  173. local html_code_string = code:sub(sepend + 1):gsub(' ', '␣')
  174. cmark.node_set_literal(markdown_code, markdown_code_string)
  175. cmark.node_set_fence_info(markdown_code, 'markdown')
  176. cmark.node_set_literal(html_code, html_code_string)
  177. cmark.node_set_fence_info(html_code, 'html')
  178. local example_div, leftcol_div, rightcol_div
  179. if format == 'latex' then
  180. 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})
  181. 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})
  182. 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})
  183. cmark.node_append_child(leftcol_div, markdown_code)
  184. cmark.node_append_child(rightcol_div, html_code)
  185. cmark.node_append_child(example_div, leftcol_div)
  186. cmark.node_append_child(example_div, rightcol_div)
  187. else
  188. leftcol_div = make_html_block('div', {{'class','column'}})
  189. rightcol_div = make_html_block('div', {{'class', 'column'}})
  190. cmark.node_append_child(leftcol_div, markdown_code)
  191. cmark.node_append_child(rightcol_div, html_code)
  192. local examplenum_div = make_html_block('div', {{'class', 'examplenum'}})
  193. local interact_link = make_html_inline('a', {{'class', 'dingus'},
  194. {'title', 'open in interactive dingus'}})
  195. cmark.node_append_child(interact_link, make_text("(interact)"))
  196. local examplenum_link = cmark.node_new(cmark.NODE_LINK)
  197. cmark.node_set_url(examplenum_link, '#example-' .. tostring(example))
  198. cmark.node_append_child(examplenum_link,
  199. make_text("Example " .. tostring(example)))
  200. cmark.node_append_child(examplenum_div, examplenum_link)
  201. if format == 'html' then
  202. cmark.node_append_child(examplenum_div, interact_link)
  203. end
  204. example_div = make_html_block('div', {{'class', 'example'},
  205. {'id','example-' .. tostring(example)}})
  206. cmark.node_append_child(example_div, examplenum_div)
  207. cmark.node_append_child(example_div, leftcol_div)
  208. cmark.node_append_child(example_div, rightcol_div)
  209. end
  210. cmark.node_insert_before(cur, example_div)
  211. cmark.node_unlink(cur)
  212. cmark.node_free(cur)
  213. elseif node_type == cmark.NODE_HTML_BLOCK and
  214. cmark.node_get_literal(cur) == '<!-- END TESTS -->\n' then
  215. -- change numbering
  216. number = {}
  217. if format ~= 'latex' then
  218. local appendices = make_html_block('div', {{'class','appendices'}})
  219. cmark.node_insert_after(cur, appendices)
  220. -- put the remaining sections in an appendix
  221. local tmp = cmark.node_next(appendices)
  222. while tmp do
  223. cmark.node_append_child(appendices, tmp)
  224. tmp = cmark.node_next(tmp)
  225. end
  226. end
  227. end
  228. end
  229. meta.toc = make_toc(toc)
  230. end
  231. local to_ref = function(ref)
  232. return '[' .. ref.label .. ']: #' .. ref.indent .. '\n'
  233. end
  234. local inp = io.read("*a")
  235. local doc1 = cmark.parse_string(inp, cmark.OPT_DEFAULT)
  236. local refs = extract_references(doc1)
  237. local refblock = '\n'
  238. for lab,ident in pairs(refs) do
  239. refblock = refblock .. '[' .. lab .. ']: #' .. ident .. '\n'
  240. -- refblock = refblock .. '[' .. lab .. 's]: #' .. ident .. '\n'
  241. end
  242. -- append references and parse again
  243. local contents, meta, msg = lcmark.convert(inp .. refblock, format,
  244. { smart = true,
  245. yaml_metadata = true,
  246. safe = false,
  247. filters = { create_anchors }
  248. })
  249. if contents then
  250. local f = io.open("tools/template." .. format, 'r')
  251. if not f then
  252. io.stderr:write("Could not find template!")
  253. os.exit(1)
  254. end
  255. local template = f:read("*a")
  256. if format == 'html' then
  257. contents = contents:gsub('␣', '<span class="space"> </span>')
  258. end
  259. meta.body = contents
  260. local rendered, msg = lcmark.render_template(template, meta)
  261. if not rendered then
  262. io.stderr:write(msg)
  263. os.exit(1)
  264. end
  265. io.write(rendered)
  266. os.exit(0)
  267. else
  268. io.stderr:write(msg)
  269. os.exit(1)
  270. end