aboutsummaryrefslogtreecommitdiff
path: root/tools/makespec.py
blob: 925f23cca20b84a0ed58e13d5ec00a310a468857 (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\n")
  64. mdlines.append("````````````````````````````````````````````````````````` markdown\n")
  65. stage = 1
  66. elif stage == 1:
  67. mdlines.append("`````````````````````````````````````````````````````````\n\n")
  68. mdlines.append("````````````````````````````````````````````````````````` html\n")
  69. stage = 2
  70. elif stage == 2:
  71. mdlines.append("`````````````````````````````````````````````````````````\n\n")
  72. mdlines.append("</div>\n")
  73. stage = 0
  74. else:
  75. sys.stderr.out("Encountered unknown stage {0}\n".format(stage))
  76. sys.exit(1)
  77. else:
  78. if stage == 0:
  79. match = re.match(r'^(#{1,6}) *(.*)', ln)
  80. if match:
  81. section = match.group(2)
  82. lastlevel = len(lastnum)
  83. level = len(match.group(1))
  84. if re.search(r'{-}$', section):
  85. section = re.sub(r' *{-} *$', '', section)
  86. if specformat == 'html':
  87. ln = re.sub(r' *{-} *$', '', ln)
  88. number = ''
  89. else:
  90. if lastlevel == level:
  91. lastnum[level - 1] = lastnum[level - 1] + 1
  92. elif lastlevel < level:
  93. while len(lastnum) < level:
  94. lastnum.append(1)
  95. else: # lastlevel > level
  96. lastnum = lastnum[0:level]
  97. lastnum[level - 1] = lastnum[level - 1] + 1
  98. number = '.'.join([str(x) for x in lastnum])
  99. ident = toIdentifier(section)
  100. ln = re.sub(r' ', ' <span class="number">' + number + '</span> ', ln, count=1)
  101. sections.append(dict(level=level,
  102. contents=section,
  103. ident=ident,
  104. number=number))
  105. refs.append("[{0}]: #{1}".format(section, ident))
  106. ln = re.sub(r'# +', '# <a id="{0}"></a>'.format(ident),
  107. ln, count=1)
  108. else:
  109. ln = re.sub(r'\[([^]]*)\]\(@([^)]*)\)', replaceAnchor, ln)
  110. else:
  111. ln = re.sub(r' ', '␣', ln)
  112. mdlines.append(ln)
  113. mdtext = ''.join(mdlines) + '\n\n' + '\n'.join(refs) + '\n'
  114. yaml = ''.join(yamllines)
  115. metadata = parseYaml(yaml)
  116. if specformat == "markdown":
  117. out(yaml + '\n\n' + mdtext)
  118. elif specformat == "html":
  119. with open("tools/template.html", "r", encoding="utf-8") as templatefile:
  120. template = Template(templatefile.read())
  121. toclines = []
  122. for section in sections:
  123. indent = ' ' * (section['level'] - 1)
  124. toclines.append(indent + '* [' + section['number'] + ' ' +
  125. section['contents'] + '](#' + section['ident'] + ')')
  126. toc = '<div id="TOC">\n\n' + '\n'.join(toclines) + '\n\n</div>\n\n'
  127. prog = "cmark --smart"
  128. result = pipe_through_prog(prog, toc + mdtext)
  129. if result == '':
  130. err("Error converting markdown version of spec to HTML.\n")
  131. exit(1)
  132. else:
  133. result = re.sub(r'␣', '<span class="space"> </span>', result)
  134. result = re.sub(r'<h([1-6])><a id="([^\"]*)"><\/a> ',
  135. "<h\\1 id=\"\\2\">", result)
  136. # put plural s inside links for better visuals:
  137. result = re.sub(r'<\/a>s', "s</a>", result)
  138. out(template.substitute(metadata, body=result))
  139. # check for errors:
  140. idents = []
  141. for ident in re.findall(r'id="([^"]*)"', result):
  142. if ident in idents:
  143. err("WARNING: duplicate identifier '" + ident + "'\n")
  144. else:
  145. idents.append(ident)
  146. for href in re.findall(r'href="#([^"]*)"', result):
  147. if not (href in idents):
  148. err("WARNING: internal link with no anchor '" + href + "'\n")
  149. reftexts = []
  150. for ref in refs:
  151. ref = re.sub('].*',']',ref).upper()
  152. if ref in reftexts:
  153. err("WARNING: duplicate reference link '" + ref + "'\n")
  154. else:
  155. reftexts.append(ref)
  156. exit(0)