BOSL2/scripts/docs_gen.py
2019-04-18 23:45:46 -07:00

682 lines
23 KiB
Python
Executable file

#!/usr/bin/env python
from __future__ import print_function
import os
import re
import sys
import math
import random
import os.path
import argparse
import subprocess
def get_header_link(name):
refpat = re.compile("[^a-z0-9_ -]")
return refpat.sub("", name.lower()).replace(" ", "-")
def toc_entry(name, indent, count=None):
lname = "{0}{1}".format(
("%d. " % count) if count else "",
name
)
ref = get_header_link(lname)
if name.endswith( (")", "}", "]") ):
name = "`" + name.replace("\\", "") + "`"
return "{0}{1} [{2}](#{3})".format(
indent,
("%d." % count) if count else "-",
name,
ref
)
def mkdn_esc(txt):
out = ""
quotpat = re.compile(r'([^`]*)(`[^`]*`)(.*$)');
while txt:
m = quotpat.match(txt)
if m:
out += m.group(1).replace(r'_', r'\_')
out += m.group(2)
txt = m.group(3)
else:
out += txt.replace(r'_', r'\_')
txt = ""
return out
def get_comment_block(lines, prefix, blanks=1):
out = []
blankcnt = 0
while lines:
if not lines[0].startswith(prefix + " "):
break
line = lines.pop(0).rstrip().lstrip("/")
if line == "":
blankcnt += 1
if blankcnt >= blanks:
break
else:
blankcnt = 0
line = line[len(prefix):]
out.append(line)
return (lines, out)
class ImageProcessing(object):
def __init__(self):
self.examples = []
self.commoncode = []
self.imgroot = ""
self.keep_scripts = False
def set_keep_scripts(self, x):
self.keep_scripts = x
def add_image(self, libfile, imgfile, code, extype):
self.examples.append((libfile, imgfile, code, extype))
def set_commoncode(self, code):
self.commoncode = code
def process_examples(self, imgroot):
self.imgroot = imgroot
for libfile, imgfile, code, extype in self.examples:
self.gen_example_image(libfile, imgfile, code, extype)
def gen_example_image(self, libfile, imgfile, code, extype):
OPENSCAD = "/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD"
CONVERT = "/usr/local/bin/convert"
COMPARE = "/usr/local/bin/compare"
if extype == "NORENDER":
return
scriptfile = "tmp_{0}.scad".format(imgfile.replace(".", "_"))
stdlibs = ["constants.scad", "math.scad", "transforms.scad", "shapes.scad", "debug.scad"]
script = ""
for lib in stdlibs:
script += "include <BOSL2/%s>\n" % lib
if libfile not in stdlibs:
script += "include <BOSL2/%s>\n" % libfile
for line in self.commoncode:
script += line+"\n"
for line in code:
script += line+"\n"
with open(scriptfile, "w") as f:
f.write(script)
if "Med" in extype:
imgsizes = ["800,600", "400x300"]
elif "Big" in extype:
imgsizes = ["1280,960", "640x480"]
elif "distribute" in script:
print(script)
imgsizes = ["800,600", "400x300"]
else: # Small
imgsizes = ["480,360", "240x180"]
print("")
print("{}: {}".format(libfile, imgfile))
tmpimgs = []
if "Spin" in extype:
for ang in range(0,359,10):
tmpimgfile = "{0}tmp_{2}_{1}.png".format(self.imgroot, ang, imgfile.replace(".", "_"))
arad = ang * math.pi / 180;
eye = "{0},{1},{2}".format(
500*math.cos(arad),
500*math.sin(arad),
500 if "Flat" in extype else 500*math.sin(arad)
)
scadcmd = [
OPENSCAD,
"-o", tmpimgfile,
"--imgsize={}".format(imgsizes[0]),
"--hardwarnings",
"--projection=o",
"--view=axes,scales",
"--autocenter",
"--viewall",
"--camera", eye+",0,0,0"
]
if "FR" in extype: # Force render
scadcmd.extend(["--render", ""])
scadcmd.append(scriptfile)
print(" ".join(scadcmd))
res = subprocess.call(scadcmd)
if res != 0:
print(script)
sys.exit(res)
tmpimgs.append(tmpimgfile)
else:
tmpimgfile = self.imgroot + "tmp_" + imgfile
scadcmd = [
OPENSCAD,
"-o", tmpimgfile,
"--imgsize={}".format(imgsizes[0]),
"--hardwarnings",
"--projection=o",
"--view=axes,scales",
"--autocenter",
"--viewall"
]
if "2D" in extype: # 2D viewpoint
scadcmd.extend(["--camera", "0,0,0,0,0,0,500"])
if "FR" in extype: # Force render
scadcmd.extend(["--render", ""])
scadcmd.append(scriptfile)
print(" ".join(scadcmd))
res = subprocess.call(scadcmd)
if res != 0:
print(script)
sys.exit(res)
tmpimgs.append(tmpimgfile)
if not self.keep_scripts:
os.unlink(scriptfile)
targimgfile = self.imgroot + imgfile
newimgfile = self.imgroot + "_new_" + imgfile
if len(tmpimgs) == 1:
cnvcmd = [CONVERT, tmpimgfile, "-resize", imgsizes[1], newimgfile]
print(" ".join(cnvcmd))
res = subprocess.call(cnvcmd)
if res != 0:
sys.exit(res)
os.unlink(tmpimgs.pop(0))
else:
cnvcmd = [
CONVERT,
"-delay", "25",
"-loop", "0",
"-coalesce",
"-scale", imgsizes[1],
"-fuzz", "2%",
"+dither",
"-layers", "Optimize",
"+map"
]
cnvcmd.extend(tmpimgs)
cnvcmd.append(newimgfile)
print(" ".join(cnvcmd))
res = subprocess.call(cnvcmd)
if res != 0:
sys.exit(res)
for tmpimg in tmpimgs:
os.unlink(tmpimg)
# Time to compare image.
if not os.path.isfile(targimgfile):
print("NEW IMAGE installed.")
os.rename(newimgfile, targimgfile)
else:
if targimgfile.endswith(".gif"):
cmpcmd = ["cmp", newimgfile, targimgfile]
print(" ".join(cmpcmd))
res = subprocess.call(cmpcmd)
issame = res == 0
else:
cmpcmd = [COMPARE, "-metric", "MAE", newimgfile, targimgfile, "null:"]
print(" ".join(cmpcmd))
p = subprocess.Popen(cmpcmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
issame = p.stdout.read().strip() == "0 (0)"
if issame:
print("Image unchanged.")
os.unlink(newimgfile)
else:
print("Image UPDATED.")
os.unlink(targimgfile)
os.rename(newimgfile, targimgfile)
imgprc = ImageProcessing()
class LeafNode(object):
def __init__(self):
self.name = ""
self.leaftype = ""
self.status = ""
self.description = []
self.usages = []
self.arguments = []
self.side_effects = []
self.examples = []
@classmethod
def match_line(cls, line, prefix):
if line.startswith(prefix + "Constant: "):
return True
if line.startswith(prefix + "Function: "):
return True
if line.startswith(prefix + "Module: "):
return True
return False
def add_example(self, title, code, extype):
self.examples.append((title, code, extype))
def parse_lines(self, lines, prefix):
blankcnt = 0
expat = re.compile(r"^(Examples?)(\(([^\)]*)\))?: *(.*)$")
while lines:
if prefix and not lines[0].startswith(prefix.strip()):
break
line = lines.pop(0).rstrip()
if line.lstrip("/").strip() == "":
blankcnt += 1
if blankcnt >= 2:
break
continue
blankcnt = 0
line = line[len(prefix):]
if line.startswith("Constant:"):
leaftype, title = line.split(":", 1)
self.name = title.strip()
self.leaftype = leaftype.strip()
if line.startswith("Function:"):
leaftype, title = line.split(":", 1)
self.name = title.strip()
self.leaftype = leaftype.strip()
if line.startswith("Module:"):
leaftype, title = line.split(":", 1)
self.name = title.strip()
self.leaftype = leaftype.strip()
if line.startswith("Status:"):
dummy, status = line.split(":", 1)
self.status = status.strip()
if line.startswith("Description:"):
dummy, desc = line.split(":", 1)
desc = desc.strip()
if desc:
self.description.append(desc)
lines, block = get_comment_block(lines, prefix)
self.description.extend(block)
if line.startswith("Usage:"):
dummy, title = line.split(":", 1)
title = title.strip()
lines, block = get_comment_block(lines, prefix)
self.usages.append([title, block])
if line.startswith("Arguments:"):
lines, block = get_comment_block(lines, prefix)
for line in block:
if "=" not in line:
print("Error: bad argument line:")
print(line)
sys.exit(2)
argname, argdesc = line.split("=", 1)
argname = argname.strip()
argdesc = argdesc.strip()
self.arguments.append([argname, argdesc])
if line.startswith("Side Effects:"):
lines, block = get_comment_block(lines, prefix)
self.side_effects.extend(block)
m = expat.match(line)
if m: # Example(TYPE):
plural = m.group(1) == "Examples"
extype = m.group(3)
title = m.group(4)
lines, block = get_comment_block(lines, prefix)
if not extype:
extype = "3D" if self.leaftype == "Module" else "NORENDER"
if not plural:
self.add_example(title=title, code=block, extype=extype)
else:
for line in block:
self.add_example(title="", code=[line], extype=extype)
return lines
def gen_md(self, fileroot, imgroot):
out = []
if self.name:
out.append("### " + mkdn_esc(self.name))
out.append("")
if self.status:
out.append("**{0}**".format(mkdn_esc(self.status)))
out.append("")
for title, usages in self.usages:
if not title:
title = "Usage"
out.append("**{0}**:".format(mkdn_esc(title)))
for usage in usages:
out.append("- {0}".format(mkdn_esc(usage)))
out.append("")
if self.description:
out.append("**Description**:")
for line in self.description:
out.append(mkdn_esc(line))
out.append("")
if self.arguments:
out.append("Argument | What it does")
out.append("--------------- | ------------------------------")
for argname, argdesc in self.arguments:
argname = argname.replace(" / ", "` / `")
out.append(
"{0:15s} | {1}".format(
"`{0}`".format(argname),
mkdn_esc(argdesc)
)
)
out.append("")
if self.side_effects:
out.append("**Side Effects**:")
for sfx in self.side_effects:
out.append("- " + mkdn_esc(sfx))
out.append("")
exnum = 0
for title, excode, extype in self.examples:
exnum += 1
if len(self.examples) < 2:
extitle = "**Example**:"
else:
extitle = "**Example {0}**:".format(exnum)
if title:
extitle += " " + mkdn_esc(title)
out.append(extitle)
out.append("")
for line in excode:
out.append(" " + line)
out.append("")
san_name = re.sub(r"[^A-Za-z0-9_]", "", self.name)
imgfile = "{0}{1}.{2}".format(
san_name,
("_%d" % exnum) if exnum > 1 else "",
"gif" if "Spin" in extype else "png"
)
if extype != "NORENDER":
out.append(
"![{0} Example{1}]({2}{3})".format(
mkdn_esc(self.name),
(" %d" % exnum) if len(self.examples) > 1 else "",
imgroot,
imgfile
)
)
out.append("")
imgprc.add_image(fileroot+".scad", imgfile, excode, extype)
out.append("---")
out.append("")
return out
class Section(object):
fignum = 0
def __init__(self):
self.name = ""
self.description = []
self.leaf_nodes = []
self.figures = []
@classmethod
def match_line(cls, line, prefix):
if line.startswith(prefix + "Section: "):
return True
return False
def add_figure(self, figtitle, figcode, figtype):
self.figures.append((figtitle, figcode, figtype))
def parse_lines(self, lines, prefix):
line = lines.pop(0).rstrip()
dummy, title = line.split(": ", 1)
self.name = title.strip()
lines, block = get_comment_block(lines, prefix, blanks=2)
self.description.extend(block)
blankcnt = 0
figpat = re.compile(r"^(Figures?)(\(([^\)]*)\))?: *(.*)$")
while lines:
if prefix and not lines[0].startswith(prefix.strip()):
break
line = lines.pop(0).rstrip()
if line.lstrip("/").strip() == "":
blankcnt += 1
if blankcnt >= 2:
break
continue
blankcnt = 0
line = line[len(prefix):]
m = figpat.match(line)
if m: # Figures(TYPE):
plural = m.group(1) == "Figures"
figtype = m.group(3)
title = m.group(4)
lines, block = get_comment_block(lines, prefix)
if not figtype:
figtype = "3D" if self.figtype == "Module" else "NORENDER"
if not plural:
self.add_figure(title, block, figtype)
else:
for line in block:
self.add_figure("", [line], figtype)
return lines
def gen_md_toc(self, count):
indent=""
out = []
if self.name:
out.append(toc_entry(self.name, indent, count=count))
indent += " "
for node in self.leaf_nodes:
out.append(toc_entry(node.name, indent))
out.append("")
return out
def gen_md(self, count, fileroot, imgroot):
out = []
if self.name:
out.append("# %d. %s" % (count, mkdn_esc(self.name)))
out.append("")
if self.description:
in_block = False
for line in self.description:
if line.startswith("```"):
in_block = not in_block
if in_block or line.startswith(" "):
out.append(line)
else:
out.append(mkdn_esc(line))
out.append("")
for title, figcode, figtype in self.figures:
Section.fignum += 1
figtitle = "**Figure {0}**:".format(Section.fignum)
if title:
figtitle += " " + mkdn_esc(title)
out.append(figtitle)
out.append("")
imgfile = "{}{}.{}".format(
"figure",
Section.fignum,
"gif" if "Spin" in figtype else "png"
)
if figtype != "NORENDER":
out.append(
"![{0} Figure {1}]({2}{3})".format(
mkdn_esc(self.name),
Section.fignum,
imgroot,
imgfile
)
)
out.append("")
imgprc.add_image(fileroot+".scad", imgfile, figcode, figtype)
in_block = False
for node in self.leaf_nodes:
out += node.gen_md(fileroot, imgroot)
return out
class LibFile(object):
def __init__(self):
self.name = ""
self.description = []
self.commoncode = []
self.sections = []
self.dep_sect = None
def parse_lines(self, lines, prefix):
currsect = None
constpat = re.compile(r"^([A-Z_0-9][A-Z_0-9]*) *=.* // (.*$)")
while lines:
while lines and prefix and not lines[0].startswith(prefix.strip()):
line = lines.pop(0)
m = constpat.match(line)
if m:
if currsect == None:
currsect = Section()
self.sections.append(currsect)
node = LeafNode();
node.extype = "Constant"
node.name = m.group(1).strip()
node.description.append(m.group(2).strip())
currsect.leaf_nodes.append(node)
# Check for LibFile header.
if lines and lines[0].startswith(prefix + "LibFile: "):
line = lines.pop(0).rstrip()
dummy, title = line.split(": ", 1)
self.name = title.strip()
lines, block = get_comment_block(lines, prefix, blanks=2)
self.description.extend(block)
# Check for CommonCode header.
if lines and lines[0].startswith(prefix + "CommonCode:"):
lines.pop(0)
lines, block = get_comment_block(lines, prefix)
self.commoncode.extend(block)
# Check for Section header.
if lines and Section.match_line(lines[0], prefix):
sect = Section()
lines = sect.parse_lines(lines, prefix)
self.sections.append(sect)
currsect = sect
# Check for LeafNode.
if lines and LeafNode.match_line(lines[0], prefix):
node = LeafNode()
lines = node.parse_lines(lines, prefix)
deprecated = node.status.startswith("DEPRECATED")
if deprecated:
if self.dep_sect == None:
self.dep_sect = Section()
self.dep_sect.name = "Deprecations"
sect = self.dep_sect
else:
if currsect == None:
currsect = Section()
self.sections.append(currsect)
sect = currsect
sect.leaf_nodes.append(node)
if lines:
lines.pop(0)
return lines
def gen_md(self, fileroot, imgroot):
imgprc.set_commoncode(self.commoncode)
out = []
if self.name:
out.append("# Library File " + mkdn_esc(self.name))
out.append("")
if self.description:
in_block = False
for line in self.description:
if line.startswith("```"):
in_block = not in_block
if in_block or line.startswith(" "):
out.append(line)
else:
out.append(mkdn_esc(line))
out.append("")
in_block = False
if self.name or self.description:
out.append("---")
out.append("")
if self.sections or self.dep_sect:
out.append("# Table of Contents")
out.append("")
cnt = 0
for sect in self.sections:
cnt += 1
out += sect.gen_md_toc(cnt)
if self.dep_sect:
cnt += 1
out += self.dep_sect.gen_md_toc(cnt)
out.append("---")
out.append("")
cnt = 0
for sect in self.sections:
cnt += 1
out += sect.gen_md(cnt, fileroot, imgroot)
if self.dep_sect:
cnt += 1
out += self.dep_sect.gen_md(cnt, fileroot, imgroot)
return out
def processFile(infile, outfile=None, gen_imgs=False, imgroot="", prefix=""):
if imgroot and not imgroot.endswith('/'):
imgroot += "/"
libfile = LibFile()
with open(infile, "r") as f:
lines = f.readlines()
libfile.parse_lines(lines, prefix)
if outfile == None:
f = sys.stdout
else:
f = open(outfile, "w")
fileroot = os.path.splitext(os.path.basename(infile))[0]
outdata = libfile.gen_md(fileroot, imgroot)
for line in outdata:
print(line, file=f)
if gen_imgs:
imgprc.process_examples(imgroot)
if outfile:
f.close()
def main():
parser = argparse.ArgumentParser(prog='docs_gen')
parser.add_argument('-k', '--keep-scripts', action="store_true",
help="If given, don't delete the temporary image OpenSCAD scripts.")
parser.add_argument('-c', '--comments-only', action="store_true",
help='If given, only process lines that start with // comments.')
parser.add_argument('-i', '--images', action="store_true",
help='If given, generate images for examples with OpenSCAD.')
parser.add_argument('-I', '--imgroot', default="",
help='The directory to put generated images in.')
parser.add_argument('-o', '--outfile',
help='Output file, if different from infile.')
parser.add_argument('infile', help='Input filename.')
args = parser.parse_args()
imgprc.set_keep_scripts(args.keep_scripts)
processFile(
args.infile,
outfile=args.outfile,
gen_imgs=args.images,
imgroot=args.imgroot,
prefix="// " if args.comments_only else ""
)
sys.exit(0)
if __name__ == "__main__":
main()
# vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap