#!/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)[len(prefix)+1:] if line == "": blankcnt += 1 if blankcnt >= blanks: break else: blankcnt = 0 out.append(line.rstrip()) 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 = ["std.scad", "debug.scad"] script = "" for lib in stdlibs: script += "include \n" % lib if libfile not in stdlibs: script += "include \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 or "show_anchors" 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.anchors = [] 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("Extra Anchors:"): lines, block = get_comment_block(lines, prefix) for line in block: if "=" not in line: print("Error: bad anchor line:") print(line) sys.exit(2) anchorname, anchordesc = line.split("=", 1) anchorname = anchorname.strip() anchordesc = anchordesc.strip() self.anchors.append([anchorname, anchordesc]) 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("") if self.anchors: out.append("Anchor Name | Description") out.append("--------------- | ------------------------------") for anchorname, anchordesc in self.anchors: anchorname = anchorname.replace(" / ", "` / `") out.append( "{0:15s} | {1}".format( "`{0}`".format(anchorname), mkdn_esc(anchordesc) ) ) 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