aboutsummaryrefslogtreecommitdiff
path: root/tools/makespec.py
blob: aa75b35b05bf2b4f6b190ce9b82541faa30b23b5 (plain)
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. import re
  4. import sys
  5. from subprocess import *
  6. from string import Template
  7. def out(str):
  8. sys.stdout.buffer.write(str.encode('utf-8'))
  9. def err(str):
  10. sys.stderr.buffer.write(str.encode('utf-8'))
  11. if len(sys.argv) == 2:
  12. specformat = sys.argv[1]
  13. if not (specformat in ["html", "markdown"]):
  14. err("Format must be html or markdown\n")
  15. exit(1)
  16. else:
  17. err("Usage: makespec.py [html|markdown]\n")
  18. exit(1)
  19. def toIdentifier(s):
  20. return re.sub(r'\s+', '-', re.sub(r'\W+', ' ', s.strip().lower()))
  21. def parseYaml(yaml):
  22. metadata = {}
  23. def parseField(match):
  24. key = match.group(1)
  25. val = match.group(2).strip()
  26. if re.match(r'^\'', val):
  27. val = val[1:len(val) - 1]
  28. metadata[key] = val
  29. fieldre = re.compile('^(\w+):(.*)$', re.MULTILINE)
  30. re.sub(fieldre, parseField, yaml)
  31. return metadata
  32. def pipe_through_prog(prog, text):
  33. result = check_output(prog.split(), input=text.encode('utf-8'))
  34. return result.decode('utf-8')
  35. def replaceAnchor(match):
  36. refs.append("[{0}]: #{1}".format(match.group(1), match.group(2)))
  37. if specformat == "html":
  38. return '<a id="{1}" href="#{1}" class="definition">{0}</a>'.format(match.group(1), match.group(2))
  39. else:
  40. return match.group(0)
  41. stage = 0
  42. example = 0
  43. section = ""
  44. sections = []
  45. mdlines = []
  46. refs = []
  47. lastnum = []
  48. finishedMeta = False
  49. yamllines = []
  50. with open('spec.txt', 'r', encoding='utf-8') as spec:
  51. for ln in spec:
  52. if not finishedMeta:
  53. yamllines.append(ln)
  54. if re.match(r'^\.\.\.$', ln):
  55. finishedMeta = True
  56. elif re.match(r'^\.$', ln):
  57. if stage == 0:
  58. example += 1
  59. mdlines.append("\n<div class=\"example\" id=\"example-{0}\" data-section=\"{1}\">\n".format(example, section))
  60. mdlines.append("<div class=\"examplenum\"><a href=\"#example-{0}\">Example {0}</a>".format(example))
  61. if specformat == "html":
  62. mdlines.append("&nbsp;&nbsp;<a class=\"dingus\" title=\"open in interactive dingus\">(interact)</a>")
  63. mdlines.append("</div>\n<div class=\"column\">\n\n")
  64. mdlines.append("````````````````````````````````````````````````````````` markdown\n")
  65. stage = 1
  66. elif stage == 1:
  67. mdlines.append("`````````````````````````````````````````````````````````\n\n")
  68. mdlines.append("\n</div>\n\n<div class=\"column\">\n\n")
  69. mdlines.append("````````````````````````````````````````````````````````` html\n")
  70. stage = 2
  71. elif stage == 2:
  72. mdlines.append("`````````````````````````````````````````````````````````\n\n")
  73. mdlines.append("</div>\n</div>\n")
  74. stage = 0
  75. else:
  76. sys.stderr.out("Encountered unknown stage {0}\n".format(stage))
  77. sys.exit(1)
  78. else:
  79. if stage == 0:
  80. match = re.match(r'^(#{1,6}) *(.*)', ln)
  81. if match:
  82. section = match.group(2)
  83. lastlevel = len(lastnum)
  84. level = len(match.group(1))
  85. if re.search(r'{-}$', section):
  86. section = re.sub(r' *{-} *$', '', section)
  87. if specformat == 'html':
  88. ln = re.sub(r' *{-} *$', '', ln)
  89. number = ''
  90. else:
  91. if lastlevel == level:
  92. lastnum[level - 1] = lastnum[level - 1] + 1
  93. elif lastlevel < level:
  94. while len(lastnum) < level:
  95. lastnum.append(1)
  96. else: # lastlevel > level
  97. lastnum = lastnum[0:level]
  98. lastnum[level - 1] = lastnum[level - 1] + 1
  99. number = '.'.join([str(x) for x in lastnum])
  100. ident = toIdentifier(section)
  101. ln = re.sub(r' ', ' <span class="number">' + number + '</span> ', ln, count=1)
  102. sections.append(dict(level=level,
  103. contents=section,
  104. ident=ident,
  105. number=number))
  106. refs.append("[{0}]: #{1}".format(section, ident))
  107. ln = re.sub(r'# +', '# <a id="{0}"></a>'.format(ident),
  108. ln, count=1)
  109. else:
  110. ln = re.sub(r'\[([^]]*)\]\(@([^)]*)\)', replaceAnchor, ln)
  111. else:
  112. ln = re.sub(r' ', '␣', ln)
  113. mdlines.append(ln)
  114. mdtext = ''.join(mdlines) + '\n\n' + '\n'.join(refs) + '\n'
  115. yaml = ''.join(yamllines)
  116. metadata = parseYaml(yaml)
  117. if specformat == "markdown":
  118. out(yaml + '\n\n' + mdtext)
  119. elif specformat == "html":
  120. with open("tools/template.html", "r", encoding="utf-8") as templatefile:
  121. template = Template(templatefile.read())
  122. toclines = []
  123. for section in sections:
  124. indent = ' ' * (section['level'] - 1)
  125. toclines.append(indent + '* [' + section['number'] + ' ' +
  126. section['contents'] + '](#' + section['ident'] + ')')
  127. toc = '<div id="TOC">\n\n' + '\n'.join(toclines) + '\n\n</div>\n\n'
  128. prog = "cmark --smart"
  129. result = pipe_through_prog(prog, toc + mdtext)
  130. if result == '':
  131. err("Error converting markdown version of spec to HTML.\n")
  132. exit(1)
  133. else:
  134. result = re.sub(r'␣', '<span class="space"> </span>', result)
  135. result = re.sub(r'<h([1-6])><a id="([^\"]*)"><\/a> ',
  136. "<h\\1 id=\"\\2\">", result)
  137. # put plural s inside links for better visuals:
  138. result = re.sub(r'<\/a>s', "s</a>", result)
  139. out(template.substitute(metadata, body=result))
  140. # check for errors:
  141. idents = []
  142. for ident in re.findall(r'id="([^"]*)"', result):
  143. if ident in idents:
  144. err("WARNING: duplicate identifier '" + ident + "'\n")
  145. else:
  146. idents.append(ident)
  147. for href in re.findall(r'href="#([^"]*)"', result):
  148. if not (href in idents):
  149. err("WARNING: internal link with no anchor '" + href + "'\n")
  150. reftexts = []
  151. for ref in refs:
  152. ref = re.sub('].*',']',ref).upper()
  153. if ref in reftexts:
  154. err("WARNING: duplicate reference link '" + ref + "'\n")
  155. else:
  156. reftexts.append(ref)
  157. exit(0)