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