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