├── setup.py ├── openscad_docsgen ├── target.py ├── utils.py ├── errorlog.py ├── target_githubwiki.py ├── filehashes.py ├── target_wiki.py ├── __init__.py ├── logmanager.py ├── mdimggen.py ├── imagemanager.py ├── blocks.py └── parser.py ├── LICENSE ├── pyproject.toml ├── .gitignore ├── README.rst └── WRITING_DOCS.md /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import setuptools 4 | 5 | if __name__ == "__main__": 6 | setuptools.setup() 7 | 8 | -------------------------------------------------------------------------------- /openscad_docsgen/target.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from .target_githubwiki import Target_GitHubWiki 4 | from .target_wiki import Target_Wiki 5 | 6 | 7 | default_target = "githubwiki" 8 | target_classes = { 9 | "githubwiki": Target_GitHubWiki, 10 | "wiki": Target_Wiki, 11 | } 12 | 13 | 14 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 15 | -------------------------------------------------------------------------------- /openscad_docsgen/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | def flatten(l, ltypes=(list, tuple)): 4 | ltype = type(l) 5 | l = list(l) 6 | i = 0 7 | while i < len(l): 8 | while isinstance(l[i], ltypes): 9 | if not l[i]: 10 | l.pop(i) 11 | i -= 1 12 | break 13 | else: 14 | l[i:i + 1] = l[i] 15 | i += 1 16 | return ltype(l) 17 | 18 | 19 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Revar Desmera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /openscad_docsgen/errorlog.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | import json 5 | 6 | class ErrorLog(object): 7 | NOTE = "notice" 8 | WARN = "warning" 9 | FAIL = "error" 10 | 11 | REPORT_FILE = "docsgen_report.json" 12 | 13 | def __init__(self): 14 | self.errlist = [] 15 | self.has_errors = False 16 | self.badfiles = {} 17 | 18 | def add_entry(self, file, line, msg, level): 19 | self.errlist.append( (file, line, msg, level) ) 20 | self.badfiles[file] = 1 21 | print("\n!! {} at {}:{}: {}".format(level.upper(), file, line, msg) , file=sys.stderr) 22 | sys.stderr.flush() 23 | if level == self.FAIL: 24 | self.has_errors = True 25 | 26 | def write_report(self): 27 | report = [ 28 | { 29 | "file": file, 30 | "line": line, 31 | "title": "DocsGen {}".format(level), 32 | "message": msg, 33 | "annotation_level": level 34 | } 35 | for file, line, msg, level in self.errlist 36 | ] 37 | with open(self.REPORT_FILE, "w") as f: 38 | f.write(json.dumps(report, sort_keys=False, indent=4)) 39 | 40 | def file_has_errors(self, file): 41 | return file in self.badfiles 42 | 43 | errorlog = ErrorLog() 44 | 45 | 46 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 47 | -------------------------------------------------------------------------------- /openscad_docsgen/target_githubwiki.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import re 4 | 5 | from .target_wiki import Target_Wiki 6 | 7 | 8 | class Target_GitHubWiki(Target_Wiki): 9 | def __init__(self, project_name=None, docs_dir="docs"): 10 | super().__init__(project_name=project_name, docs_dir=docs_dir) 11 | 12 | def image_block(self, item_name, title, subtitle="", code=[], code_below=False, rel_url=None, width='', height=''): 13 | out = [] 14 | out.extend(self.block_header(title, subtitle, escsub=False)) 15 | if rel_url: 16 | out.extend(self.image(item_name, title, rel_url, width=width, height=height)) 17 | if code_below: 18 | out.extend(self.markdown_block(['
'])) 19 | out.extend(self.code_block(code)) 20 | if not code_below: 21 | out.extend(self.markdown_block(['

'])) 22 | return out 23 | 24 | def image(self, item_name, img_type="", rel_url="", height='', width=''): 25 | width = ' width="{}"'.format(width) if width else '' 26 | height = ' height="{}"'.format(height) if width else '' 27 | return [ 28 | '{0} {1}'.format( 29 | self.escape_entities(item_name), 30 | self.escape_entities(img_type), 31 | rel_url, width, height 32 | ), 33 | "" 34 | ] 35 | 36 | def code_block(self, code): 37 | out = [] 38 | if code: 39 | out.extend(self.indent_lines(code)) 40 | out.append("") 41 | return out 42 | 43 | 44 | 45 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "openscad_docsgen" 7 | version = "2.0.54" 8 | authors = [ 9 | { name="Revar Desmera", email="revarbat@gmail.com" }, 10 | ] 11 | maintainers = [ 12 | { name="Revar Desmera", email="revarbat@gmail.com" }, 13 | ] 14 | description = "A processor to generate Markdown code documentation with images from OpenSCAD source comments." 15 | readme = "README.rst" 16 | license = {file = "LICENSE"} 17 | requires-python = ">=3.7" 18 | classifiers = [ 19 | "Development Status :: 5 - Production/Stable", 20 | "Environment :: Console", 21 | "Intended Audience :: Developers", 22 | "Intended Audience :: Manufacturing", 23 | "Operating System :: MacOS :: MacOS X", 24 | "Operating System :: Microsoft :: Windows", 25 | "Operating System :: POSIX", 26 | "Programming Language :: Python :: 3", 27 | "Topic :: Artistic Software", 28 | "Topic :: Multimedia :: Graphics :: 3D Modeling", 29 | "Topic :: Multimedia :: Graphics :: 3D Rendering", 30 | "Topic :: Software Development :: Libraries", 31 | "Topic :: Software Development :: Libraries :: Python Modules", 32 | ] 33 | keywords = ["openscad", "documentation generation", "docs generation", "docsgen"] 34 | dependencies = [ 35 | "pillow>=10.3.0", 36 | "PyYAML>=6.0", 37 | "scipy>=1.15.3", 38 | "imageio>=2.37.0", 39 | "openscad_runner>=1.2.2", 40 | ] 41 | 42 | [project.scripts] 43 | openscad-docsgen = "openscad_docsgen:main" 44 | openscad-mdimggen = "openscad_docsgen.mdimggen:mdimggen_main" 45 | 46 | [project.urls] 47 | "Homepage" = "https://github.com/belfryscad/openscad_docsgen" 48 | "Repository" = "https://github.com/belfryscad/openscad_docsgen" 49 | "Bug Tracker" = "https://github.com/belfryscad/openscad_docsgen/issues" 50 | "Releases" = "https://github.com/belfryscad/openscad_docsgen/releases" 51 | "Usage" = "https://github.com/belfryscad/openscad_docsgen/README.rst" 52 | "Documentation" = "https://github.com/belfryscad/openscad_docsgen/WRITING_DOCS.md" 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .*.swp 132 | 133 | -------------------------------------------------------------------------------- /openscad_docsgen/filehashes.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | import os.path 5 | import sys 6 | import hashlib 7 | 8 | class FileHashes(object): 9 | def __init__(self, hashfile): 10 | self.hashfile = hashfile 11 | self.load() 12 | 13 | def _sha256sum(self, filename): 14 | """Calculate the hash value for the given file's contents. 15 | """ 16 | h = hashlib.sha256() 17 | b = bytearray(128*1024) 18 | mv = memoryview(b) 19 | try: 20 | with open(filename, 'rb', buffering=0) as f: 21 | for n in iter(lambda : f.readinto(mv), 0): 22 | h.update(mv[:n]) 23 | except FileNotFoundError as e: 24 | pass 25 | return h.hexdigest() 26 | 27 | def load(self): 28 | """Reads all known file hash values from the hashes file. 29 | """ 30 | self.file_hashes = {} 31 | if os.path.isfile(self.hashfile): 32 | try: 33 | with open(self.hashfile, "r") as f: 34 | for line in f.readlines(): 35 | filename, hashstr = line.strip().split("|") 36 | self.file_hashes[filename] = hashstr 37 | except ValueError as e: 38 | print("Corrrupt hashes file. Ignoring.", file=sys.stderr) 39 | sys.stderr.flush() 40 | self.file_hashes = {} 41 | 42 | def save(self): 43 | """Writes out all known hash values. 44 | """ 45 | os.makedirs(os.path.dirname(self.hashfile), exist_ok=True) 46 | with open(self.hashfile, "w") as f: 47 | for filename, hashstr in self.file_hashes.items(): 48 | f.write("{}|{}\n".format(filename, hashstr)) 49 | 50 | def is_changed(self, filename): 51 | """Returns True if the given file matches it's recorded hash value. 52 | Updates the hash value in memory for the file if it doesn't match. 53 | Does NOT save hash values to disk. 54 | """ 55 | newhash = self._sha256sum(filename) 56 | if filename not in self.file_hashes: 57 | self.file_hashes[filename] = newhash 58 | return True 59 | oldhash = self.file_hashes[filename] 60 | if oldhash != newhash: 61 | self.file_hashes[filename] = newhash 62 | return True 63 | return False 64 | 65 | def invalidate(self,filename): 66 | """Invalidates the has value for the given file. 67 | """ 68 | self.file_hashes.pop(filename) 69 | 70 | 71 | 72 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 73 | -------------------------------------------------------------------------------- /openscad_docsgen/target_wiki.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import re 4 | 5 | 6 | class Target_Wiki(object): 7 | FILE = 1 8 | SECTION = 2 9 | SUBSECTION = 2 10 | ITEM = 3 11 | def __init__(self, project_name=None, docs_dir="docs"): 12 | self.docs_dir = docs_dir 13 | self.project_name = project_name 14 | 15 | def get_suffix(self): 16 | return ".md" 17 | 18 | def postprocess(self, lines): 19 | return lines 20 | 21 | def escape_entities(self, txt): 22 | """ 23 | Escapes markdown symbols for underscores, ampersands, less-than and 24 | greater-than symbols. 25 | """ 26 | out = "" 27 | quotpat = re.compile(r'([^`]*)(`[^`]*`)(.*$)') 28 | while txt: 29 | m = quotpat.match(txt) 30 | unquot = m.group(1) if m else txt 31 | literal = m.group(2) if m else "" 32 | txt = m.group(3) if m else "" 33 | unquot = unquot.replace(r'_', r'\_') 34 | unquot = unquot.replace(r'&', r'&') 35 | unquot = unquot.replace(r'<', r'<') 36 | unquot = unquot.replace(r'>', r'>') 37 | out += unquot + literal 38 | return out 39 | 40 | def bold(self, txt): 41 | return "**{}**".format(txt) 42 | 43 | def italics(self, txt): 44 | return "*{}*".format(txt) 45 | 46 | def line_with_break(self, line): 47 | if isinstance(line,list): 48 | line[-1] += " " 49 | return line 50 | return [line + " "] 51 | 52 | def quote(self, lines=[]): 53 | if isinstance(lines,list): 54 | return [">" + line for line in lines] 55 | return [">" + lines] 56 | 57 | def paragraph(self, lines=[]): 58 | lines.append("") 59 | return lines 60 | 61 | def mouseover_tags(self, tags, file=None, htag="sup", wrap="{}"): 62 | if not file: 63 | fmt = ' <{htag} title="{text}">{abbr}' 64 | elif '#' in file: 65 | fmt = ' <{htag} title="{text}">[{abbr}]({link})' 66 | else: 67 | fmt = ' <{htag} title="{text}">[{abbr}]({link}#{linktag})' 68 | out = "".join( 69 | fmt.format( 70 | htag=htag, 71 | abbr=wrap.format(tag), 72 | text=text, 73 | link=file, 74 | linktag=self.header_link(tag) 75 | ) 76 | for tag, text in tags.items() 77 | ) 78 | return out 79 | 80 | def header_link(self, name): 81 | """ 82 | Generates markdown link for a header. 83 | """ 84 | refpat = re.compile("[^a-z0-9_ -]") 85 | return refpat.sub("", name.lower()).replace(" ", "-") 86 | 87 | def indent_lines(self, lines): 88 | return [" "*4 + line for line in lines] 89 | 90 | def get_link(self, label, anchor="", file="", literalize=True, html=False): 91 | if literalize: 92 | label = "`{0}`".format(label) 93 | else: 94 | label = self.escape_entities(label) 95 | if anchor: 96 | anchor = "#" + anchor 97 | if html: 98 | return '{}'.format(file, anchor, label) 99 | return "[{0}]({1}{2})".format(label, file, anchor) 100 | 101 | def code_span(self, txt): 102 | return "{}".format(txt) 103 | 104 | def horizontal_rule(self): 105 | return [ "---", "" ] 106 | 107 | def header(self, txt, lev=1, esc=True): 108 | return [ 109 | "{} {}".format( 110 | "#" * lev, 111 | self.escape_entities(txt) if esc else txt 112 | ), 113 | "" 114 | ] 115 | 116 | def block_header(self, title, subtitle="", escsub=True): 117 | return [ 118 | "**{}:** {}".format( 119 | self.escape_entities(title), 120 | self.escape_entities(subtitle) if escsub else subtitle 121 | ), 122 | "" 123 | ] 124 | 125 | def markdown_block(self, text=[]): 126 | out = text 127 | out.append("") 128 | return out 129 | 130 | def image_block(self, item_name, title, subtitle="", code=[], code_below=False, rel_url=None, **kwargs): 131 | out = [] 132 | out.extend(self.block_header(title, subtitle)) 133 | if not code_below: 134 | out.extend(self.code_block(code)) 135 | if rel_url: 136 | out.extend(self.image(item_name, title, rel_url)) 137 | if code_below: 138 | out.extend(self.code_block(code)) 139 | return out 140 | 141 | def image(self, item_name, img_type="", rel_url="", **kwargs): 142 | return [ 143 | '![{0} {1}]({2} "{0} {1}")'.format( 144 | self.escape_entities(item_name), 145 | self.escape_entities(img_type), 146 | rel_url 147 | ), 148 | "" 149 | ] 150 | 151 | def code_block(self, code): 152 | out = [] 153 | if code: 154 | out.append("``` {.C linenos=True}") 155 | out.extend(code) 156 | out.append("```") 157 | out.append("") 158 | return out 159 | 160 | def bullet_list_start(self): 161 | return [] 162 | 163 | def bullet_list_item(self, item): 164 | out = ["- {}".format(item)] 165 | return out 166 | 167 | def bullet_list_end(self): 168 | return [""] 169 | 170 | def bullet_list(self, items): 171 | out = self.bullet_list_start() 172 | for item in items: 173 | out.extend(self.bullet_list_item(item)) 174 | out.extend(self.bullet_list_end()) 175 | return out 176 | 177 | def numbered_list_start(self): 178 | return [] 179 | 180 | def numbered_list_item(self, num, item): 181 | out = [ 182 | "{}. {}".format(num, item) 183 | ] 184 | return out 185 | 186 | def numbered_list_end(self): 187 | return [""] 188 | 189 | def numbered_list(self, items): 190 | out = self.numbered_list_start() 191 | for num, item in enumerate(items): 192 | out.extend(self.numbered_list_item(num+1, item)) 193 | out.extend(self.numbered_list_end()) 194 | return out 195 | 196 | def table(self, headers, rows): 197 | out = [] 198 | hcells = [] 199 | lcells = [] 200 | for hdr in headers: 201 | if hdr.startswith("^"): 202 | hdr = hdr.lstrip("^") 203 | hcells.append(hdr) 204 | lcells.append("-"*min(20,len(hdr))) 205 | out.append(" | ".join(hcells)) 206 | out.append(" | ".join(lcells)) 207 | for row in rows: 208 | fcells = [] 209 | for i, cell in enumerate(row): 210 | hdr = headers[i] 211 | if hdr.startswith("^"): 212 | cell = " / ".join( 213 | "{:20s}".format("`{}`".format(x.strip())) 214 | for x in cell.split("/") 215 | ) 216 | fcells.append(cell) 217 | out.append( " | ".join(fcells) ) 218 | out.append("") 219 | return out 220 | 221 | 222 | 223 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 224 | -------------------------------------------------------------------------------- /openscad_docsgen/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import print_function 4 | 5 | import os 6 | import sys 7 | import glob 8 | import os.path 9 | import argparse 10 | import platform 11 | 12 | from .errorlog import ErrorLog, errorlog 13 | from .parser import DocsGenParser, DocsGenException 14 | from .target import default_target, target_classes 15 | from .logmanager import log_manager 16 | 17 | 18 | class Options(object): 19 | def __init__(self, args): 20 | self.files = args.srcfiles 21 | self.target_profile = args.target_profile 22 | self.project_name = args.project_name 23 | self.docs_dir = args.docs_dir.rstrip("/") 24 | self.quiet = args.quiet 25 | self.force = args.force 26 | self.strict = args.strict 27 | self.test_only = args.test_only 28 | self.gen_imgs = not args.no_images 29 | self.gen_files = args.gen_files 30 | self.gen_toc = args.gen_toc 31 | self.gen_index = args.gen_index 32 | self.gen_topics = args.gen_topics 33 | self.gen_glossary = args.gen_glossary 34 | self.gen_cheat = args.gen_cheat 35 | self.gen_sidebar = args.gen_sidebar 36 | self.report = args.report 37 | self.dump_tree = args.dump_tree 38 | self.png_animation = args.png_animation 39 | self.verbose = args.verbose 40 | self.enabled_features = [item.strip() for item in args.enabled_features.split(",")] 41 | self.sidebar_header = [] 42 | self.sidebar_middle = [] 43 | self.sidebar_footer = [] 44 | self.update_target() 45 | 46 | def set_target(self, targ): 47 | if targ not in target_classes: 48 | return False 49 | self.target_profile = targ 50 | return True 51 | 52 | def update_target(self): 53 | self.target = target_classes[self.target_profile]( 54 | project_name=self.project_name, 55 | docs_dir=self.docs_dir 56 | ) 57 | 58 | def processFiles(opts): 59 | docsgen = DocsGenParser(opts) 60 | # DocsGenParser may change opts settings, based on the _rc file. 61 | 62 | if not opts.files: 63 | opts.files = glob.glob("*.scad") 64 | elif platform.system() == 'Windows': 65 | opts.files = [file for src_file in opts.files for file in glob.glob(src_file)] 66 | 67 | fail = False 68 | for infile in opts.files: 69 | if not os.path.exists(infile): 70 | print("{} does not exist.".format(infile)) 71 | fail = True 72 | elif not os.path.isfile(infile): 73 | print("{} is not a file.".format(infile)) 74 | fail = True 75 | elif not os.access(infile, os.R_OK): 76 | print("{} is not readable.".format(infile)) 77 | fail = True 78 | if fail: 79 | sys.exit(-1) 80 | 81 | docsgen.parse_files(opts.files, False) 82 | 83 | if opts.dump_tree: 84 | docsgen.dump_full_tree() 85 | log_manager.process_requests(test_only=opts.test_only) 86 | 87 | if opts.gen_files or opts.test_only: 88 | docsgen.write_docs_files() 89 | if opts.gen_toc: 90 | docsgen.write_toc_file() 91 | if opts.gen_index: 92 | docsgen.write_index_file() 93 | if opts.gen_topics: 94 | docsgen.write_topics_file() 95 | if opts.gen_glossary: 96 | docsgen.write_glossary_file() 97 | if opts.gen_cheat: 98 | docsgen.write_cheatsheet_file() 99 | if opts.gen_sidebar: 100 | docsgen.write_sidebar_file() 101 | 102 | if opts.report: 103 | errorlog.write_report() 104 | if errorlog.has_errors: 105 | print("WARNING: Errors encountered.", file=sys.stderr) 106 | sys.exit(-1) 107 | 108 | 109 | def main(): 110 | target_profiles = ["githubwiki", "stdwiki"] 111 | 112 | parser = argparse.ArgumentParser(prog='openscad-docsgen', ) 113 | parser.add_argument('-D', '--docs-dir', default="docs", 114 | help='The directory to put generated documentation in.') 115 | parser.add_argument('-T', '--test-only', action="store_true", 116 | help="If given, don't generate images, but do try executing the scripts.") 117 | parser.add_argument('-q', '--quiet', action="store_true", 118 | help="Suppress printing of progress data.") 119 | parser.add_argument('-S', '--strict', action="store_true", 120 | help="If given, require File/LibFile and Section headers.") 121 | parser.add_argument('-f', '--force', action="store_true", 122 | help='If given, force regeneration of images.') 123 | parser.add_argument('-n', '--no-images', action="store_true", 124 | help='If given, skips image generation.') 125 | parser.add_argument('-m', '--gen-files', action="store_true", 126 | help='If given, generate documents for each source file.') 127 | parser.add_argument('-i', '--gen-index', action="store_true", 128 | help='If given, generate AlphaIndex.md file.') 129 | parser.add_argument('-I', '--gen-topics', action="store_true", 130 | help='If given, generate Topics.md topics index file.') 131 | parser.add_argument('-t', '--gen-toc', action="store_true", 132 | help='If given, generate TOC.md table of contents file.') 133 | parser.add_argument('-g', '--gen-glossary', action="store_true", 134 | help='If given, generate Glossary.md file.') 135 | parser.add_argument('-c', '--gen-cheat', action="store_true", 136 | help='If given, generate CheatSheet.md file with all Usage lines.') 137 | parser.add_argument('-s', '--gen_sidebar', action="store_true", 138 | help="If given, generate _Sidebar.md file index.") 139 | parser.add_argument('-a', '--png-animation', action="store_true", 140 | help='If given, animations are created using animated PNGs instead of GIFs.') 141 | parser.add_argument('-P', '--project-name', 142 | help='If given, sets the name of the project to be shown in titles.') 143 | parser.add_argument('-r', '--report', action="store_true", 144 | help='If given, write all warnings and errors to docsgen_report.json') 145 | parser.add_argument('-d', '--dump-tree', action="store_true", 146 | help='If given, dumps the documentation tree for debugging.') 147 | parser.add_argument('-p', '--target-profile', choices=target_classes.keys(), default=default_target, 148 | help='Sets the output target profile. Defaults to "{}"'.format(default_target)) 149 | parser.add_argument('-e', '--enabled_features', default='', help='List of enabled experimental features') 150 | parser.add_argument('-v', '--verbose', help='Dump the openscad commands', action="store_true") 151 | parser.add_argument('srcfiles', nargs='*', help='List of input source files.') 152 | opts = Options(parser.parse_args()) 153 | 154 | try: 155 | processFiles(opts) 156 | except DocsGenException as e: 157 | print(e) 158 | sys.exit(-1) 159 | except OSError as e: 160 | print(e) 161 | sys.exit(-1) 162 | except KeyboardInterrupt as e: 163 | print(" Aborting.", file=sys.stderr) 164 | sys.exit(-1) 165 | 166 | sys.exit(0) 167 | 168 | 169 | if __name__ == "__main__": 170 | main() 171 | 172 | 173 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 174 | -------------------------------------------------------------------------------- /openscad_docsgen/logmanager.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | import re 5 | import tempfile 6 | import subprocess 7 | import sys 8 | import shutil 9 | import platform 10 | from .errorlog import errorlog, ErrorLog 11 | 12 | class LogRequest(object): 13 | #_echo_re = re.compile(r"ECHO:\s*(.+)$") 14 | _echo_re = re.compile(r"ECHO:\s*(.+?)(?=\nECHO:|$)", re.DOTALL) 15 | 16 | def __init__(self, src_file, src_line, script_lines, starting_cb=None, completion_cb=None, verbose=False): 17 | self.src_file = src_file 18 | self.src_line = src_line 19 | self.script_lines = [ 20 | line[2:] if line.startswith("--") else line 21 | for line in script_lines 22 | ] 23 | self.starting_cb = starting_cb 24 | self.completion_cb = completion_cb 25 | self.verbose = verbose 26 | 27 | self.complete = False 28 | self.status = "INCOMPLETE" 29 | self.success = False 30 | self.cmdline = [] 31 | self.return_code = None 32 | self.stdout = [] 33 | self.stderr = [] 34 | self.echos = [] 35 | self.warnings = [] 36 | self.errors = [] 37 | 38 | def starting(self): 39 | if self.starting_cb: 40 | self.starting_cb(self) 41 | 42 | def completed(self, status, stdout=None, stderr=None, return_code=None): 43 | self.complete = True 44 | self.status = status 45 | self.success = (status == "SUCCESS") 46 | self.return_code = return_code 47 | self.stdout = stdout or [] 48 | self.stderr = stderr or [] 49 | self.echos = [] 50 | self.warnings = [] 51 | self.errors = [] 52 | 53 | stdout_text = "\n".join(self.stdout) 54 | for match in self._echo_re.finditer(stdout_text): 55 | echo_content = match.group(1).strip().strip('"') # Remove quotes 56 | self.echos.append(echo_content) 57 | for line in self.stderr: 58 | if self.verbose: 59 | print(f"Parsing stderr line: {line}") 60 | if "WARNING:" in line: 61 | self.warnings.append(line) 62 | elif "ERROR:" in line: 63 | self.errors.append(line) 64 | if self.completion_cb: 65 | self.completion_cb(self) 66 | 67 | 68 | class LogManager(object): 69 | def __init__(self): 70 | self.requests = [] 71 | self.test_only = False 72 | 73 | def find_openscad_binary(self): 74 | exepath = shutil.which("openscad") 75 | if exepath is not None: 76 | if self.test_only: 77 | print(f"Found OpenSCAD in PATH: {exepath}") 78 | return exepath 79 | # Platform-specific fallback paths 80 | system = platform.system() 81 | if system == "Darwin": # macOS 82 | exepath = shutil.which("/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD") 83 | if exepath is not None: 84 | if self.test_only: 85 | print(f"Found OpenSCAD in macOS path: {exepath}") 86 | return exepath 87 | elif system == "Windows": 88 | test_paths = [ 89 | r"C:\Program Files\OpenSCAD\openscad.com", 90 | r"C:\Program Files\OpenSCAD\openscad.exe", 91 | r"C:\Program Files (x86)\OpenSCAD\openscad.com", 92 | r"C:\Program Files (x86)\OpenSCAD\openscad.exe", 93 | ] 94 | for p in test_paths: 95 | exepath = shutil.which(p) 96 | if exepath is not None: 97 | if self.test_only: 98 | print(f"Found OpenSCAD in Windows path: {exepath}") 99 | return exepath 100 | else: # Linux or other 101 | test_paths = [ 102 | "/usr/bin/openscad", 103 | "/usr/local/bin/openscad", 104 | "/opt/openscad/bin/openscad" 105 | ] 106 | for p in test_paths: 107 | exepath = shutil.which(p) 108 | if exepath is not None: 109 | if self.test_only: 110 | print(f"Found OpenSCAD in Linux/other path: {exepath}") 111 | return exepath 112 | raise Exception( 113 | "Can't find OpenSCAD executable. Please install OpenSCAD and ensure it is in your system PATH " 114 | "or located in a standard directory (e.g., /Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD on macOS, " 115 | "C:\\Program Files\\OpenSCAD\\openscad.exe on Windows, /usr/bin/openscad on Linux)." 116 | ) 117 | 118 | def purge_requests(self): 119 | self.requests = [] 120 | 121 | def new_request(self, src_file, src_line, script_lines, starting_cb=None, completion_cb=None, verbose=False): 122 | req = LogRequest(src_file, src_line, script_lines, starting_cb, completion_cb, verbose=verbose) 123 | self.requests.append(req) 124 | #if verbose: 125 | # print(f"New log request created for {src_file}:{src_line}") 126 | return req 127 | 128 | def process_request(self, req): 129 | req.starting() 130 | try: 131 | openscad_bin = self.find_openscad_binary() 132 | except Exception as e: 133 | error_msg = str(e) 134 | req.completed("FAIL", [], [error_msg], -1) 135 | errorlog.add_entry(req.src_file, req.src_line, error_msg, ErrorLog.FAIL) 136 | return 137 | 138 | # Create temp file in the same directory as src_file 139 | src_dir = os.path.dirname(os.path.abspath(req.src_file)) 140 | try: 141 | with tempfile.NamedTemporaryFile(suffix=".scad", delete=False, mode="w", dir=src_dir) as temp_file: 142 | for line in req.script_lines: 143 | temp_file.write(line + "\n") 144 | script_file = temp_file.name 145 | except OSError as e: 146 | error_msg = f"Failed to create temporary file in {src_dir}: {str(e)}" 147 | req.completed("FAIL", [], [error_msg], -1) 148 | errorlog.add_entry(req.src_file, req.src_line, error_msg, ErrorLog.FAIL) 149 | return 150 | 151 | try: 152 | cmdline = [openscad_bin, "-o", "-", "--export-format=echo", script_file] 153 | if self.test_only: 154 | cmdline.append("--hardwarnings") 155 | #if req.verbose: 156 | # print(f"Executing: {' '.join(cmdline)}") 157 | process = subprocess.run( 158 | cmdline, 159 | capture_output=True, 160 | text=True, 161 | timeout=10 162 | ) 163 | stdout = process.stdout.splitlines() 164 | stderr = process.stderr.splitlines() 165 | return_code = process.returncode 166 | 167 | #if req.verbose: 168 | # print(f"OpenSCAD return code: {return_code}") 169 | # print(f"Stdout: {stdout}") 170 | # print(f"Stderr: {stderr}") 171 | 172 | if return_code != 0 or any("ERROR:" in line for line in stderr): 173 | req.completed("FAIL", stdout, stderr, return_code) 174 | else: 175 | req.completed("SUCCESS", stdout, stderr, return_code) 176 | 177 | except subprocess.TimeoutExpired: 178 | req.completed("FAIL", [], ["Timeout expired"], -1) 179 | errorlog.add_entry(req.src_file, req.src_line, "OpenSCAD execution timed out", ErrorLog.FAIL) 180 | except Exception as e: 181 | req.completed("FAIL", [], [str(e)], -1) 182 | errorlog.add_entry(req.src_file, req.src_line, f"OpenSCAD execution failed: {str(e)}", ErrorLog.FAIL) 183 | finally: 184 | if os.path.exists(script_file): 185 | os.unlink(script_file) 186 | 187 | def process_requests(self, test_only=False): 188 | self.test_only = test_only 189 | if not self.requests: 190 | if self.test_only: 191 | print("No log requests to process") 192 | for req in self.requests: 193 | self.process_request(req) 194 | self.requests = [] 195 | 196 | log_manager = LogManager() -------------------------------------------------------------------------------- /openscad_docsgen/mdimggen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import print_function 4 | 5 | import os 6 | import sys 7 | import yaml 8 | import glob 9 | import os.path 10 | import argparse 11 | import platform 12 | 13 | from .errorlog import errorlog, ErrorLog 14 | from .imagemanager import image_manager 15 | from .logmanager import log_manager 16 | from .filehashes import FileHashes 17 | 18 | 19 | class MarkdownImageGen(object): 20 | HASHFILE = ".source_hashes" 21 | 22 | def __init__(self, opts): 23 | self.opts = opts 24 | self.filehashes = FileHashes(os.path.join(self.opts.docs_dir, self.HASHFILE)) 25 | 26 | def img_started(self, req): 27 | print(" {}... ".format(os.path.basename(req.image_file)), end='') 28 | sys.stdout.flush() 29 | 30 | def img_completed(self, req): 31 | if req.success: 32 | if req.status == "SKIP": 33 | print() 34 | else: 35 | print(req.status) 36 | sys.stdout.flush() 37 | return 38 | out = "\n\n" 39 | for line in req.echos: 40 | out += line + "\n" 41 | for line in req.warnings: 42 | out += line + "\n" 43 | for line in req.errors: 44 | out += line + "\n" 45 | out += "//////////////////////////////////////////////////////////////////////\n" 46 | out += "// LibFile: {} Line: {} Image: {}\n".format( 47 | req.src_file, req.src_line, os.path.basename(req.image_file) 48 | ) 49 | out += "//////////////////////////////////////////////////////////////////////\n" 50 | for line in req.script_lines: 51 | out += line + "\n" 52 | out += "//////////////////////////////////////////////////////////////////////\n" 53 | errorlog.add_entry(req.src_file, req.src_line, out, ErrorLog.FAIL) 54 | sys.stderr.flush() 55 | 56 | def log_completed(self, req): 57 | if not req.success: 58 | out = "\n".join(req.errors + req.warnings) 59 | errorlog.add_entry(req.src_file, req.src_line, out, ErrorLog.FAIL) 60 | 61 | def processFiles(self, srcfiles): 62 | opts = self.opts 63 | image_root = os.path.join(opts.docs_dir, opts.image_root) 64 | for infile in srcfiles: 65 | fileroot = os.path.splitext(os.path.basename(infile))[0] 66 | outfile = os.path.join(opts.docs_dir, opts.file_prefix + fileroot + ".md") 67 | print(outfile) 68 | sys.stdout.flush() 69 | 70 | out = [] 71 | log_requests = [] 72 | with open(infile, "r") as f: 73 | script = [] 74 | extyp = "" 75 | in_script = False 76 | imgnum = 0 77 | show_script = True 78 | linenum = -1 79 | for line in f.readlines(): 80 | linenum += 1 81 | line = line.rstrip("\n") 82 | if line.startswith("```openscad-log"): 83 | in_script = True 84 | is_log_block = True 85 | script = [] 86 | elif line.startswith("```openscad"): 87 | in_script = True; 88 | is_log_block = False 89 | if "-" in line and not line.startswith("```openscad-log"): 90 | extyp = line.split("-")[1] 91 | else: 92 | extyp = "" 93 | show_script = "ImgOnly" not in extyp 94 | script = [] 95 | imgnum = imgnum + 1 96 | elif in_script: 97 | if line == "```": 98 | in_script = False 99 | if is_log_block: 100 | req = log_manager.new_request( 101 | infile, linenum, script, 102 | completion_cb=self.log_completed, 103 | verbose=True 104 | ) 105 | log_manager.process_requests() 106 | out.append("```log") 107 | if req.success and req.echos: 108 | out.extend(req.echos) 109 | else: 110 | out.append("No log output generated.") 111 | out.append("```") 112 | else: 113 | if opts.png_animation: 114 | fext = "png" 115 | elif any(x in extyp for x in ("Anim", "Spin")): 116 | fext = "gif" 117 | else: 118 | fext = "png" 119 | fname = "{}_{}.{}".format(fileroot, imgnum, fext) 120 | img_rel_url = os.path.join(opts.image_root, fname) 121 | imgfile = os.path.join(opts.docs_dir, img_rel_url) 122 | image_manager.new_request( 123 | fileroot+".md", linenum, 124 | imgfile, script, extyp, 125 | default_colorscheme=opts.colorscheme, 126 | starting_cb=self.img_started, 127 | completion_cb=self.img_completed, 128 | verbose=opts.verbose 129 | ) 130 | if show_script: 131 | out.append("```openscad") 132 | for line in script: 133 | if not line.startswith("--"): 134 | out.append(line) 135 | out.append("```") 136 | out.append("![Figure {}]({})".format(imgnum, img_rel_url)) 137 | show_script = True 138 | extyp = "" 139 | is_log_block = False 140 | else: 141 | script.append(line) 142 | else: 143 | out.append(line) 144 | 145 | if not opts.test_only: 146 | with open(outfile, "w") as f: 147 | for line in out: 148 | print(line, file=f) 149 | 150 | has_changed = self.filehashes.is_changed(infile) 151 | if opts.force or opts.test_only or has_changed: 152 | image_manager.process_requests(test_only=opts.test_only) 153 | log_manager.process_requests(test_only=opts.test_only) 154 | image_manager.purge_requests() 155 | log_manager.purge_requests() 156 | 157 | if errorlog.file_has_errors(infile): 158 | self.filehashes.invalidate(infile) 159 | self.filehashes.save() 160 | 161 | 162 | def mdimggen_main(): 163 | rcfile = ".openscad_mdimggen_rc" 164 | defaults = {} 165 | if os.path.exists(rcfile): 166 | with open(rcfile, "r") as f: 167 | data = yaml.safe_load(f) 168 | if data is not None: 169 | defaults = data 170 | parser = argparse.ArgumentParser(prog='openscad-mdimggen') 171 | parser.add_argument('-D', '--docs-dir', default=defaults.get("docs_dir", "docs"), 172 | help='The directory to put generated documentation in.') 173 | parser.add_argument('-P', '--file-prefix', default=defaults.get("file_prefix", ""), 174 | help='The prefix to put in front of each output markdown file.') 175 | parser.add_argument('-T', '--test-only', action="store_true", 176 | help="If given, don't generate images, but do try executing the scripts.") 177 | parser.add_argument('-I', '--image_root', default=defaults.get("image_root", "images"), 178 | help='The directory to put generated images in.') 179 | parser.add_argument('-f', '--force', action="store_true", 180 | help='If given, force regeneration of images.') 181 | parser.add_argument('-a', '--png-animation', action="store_true", 182 | default=defaults.get("png_animations", True), 183 | help='If given, animations are created using animated PNGs instead of GIFs.') 184 | parser.add_argument('-v', '--verbose', help='Dump the openscad commands', action="store_true") 185 | parser.add_argument('-C', '--colorscheme', default=defaults.get("ColorScheme", "Cornfield"), 186 | help='The color scheme for rendering images (e.g., Tomorrow).') 187 | parser.add_argument('srcfiles', nargs='*', help='List of input markdown files.') 188 | args = parser.parse_args() 189 | 190 | if not args.srcfiles: 191 | srcfiles = defaults.get("source_files", []) 192 | if isinstance(srcfiles, str): 193 | args.srcfiles = glob.glob(srcfiles) 194 | elif isinstance(srcfiles, list): 195 | args.srcfiles = [] 196 | for srcfile in srcfiles: 197 | if isinstance(srcfile, str): 198 | args.srcfiles.extend(glob.glob(srcfile)) 199 | elif platform.system() == 'Windows': 200 | args.srcfiles = [file for src_file in args.srcfiles for file in glob.glob(src_file)] 201 | 202 | if not args.srcfiles: 203 | print("No files to parse. Aborting.", file=sys.stderr) 204 | sys.exit(-1) 205 | 206 | try: 207 | mdimggen = MarkdownImageGen(args) 208 | mdimggen.processFiles(args.srcfiles) 209 | except OSError as e: 210 | print(e) 211 | sys.exit(-1) 212 | except KeyboardInterrupt as e: 213 | print(" Aborting.", file=sys.stderr) 214 | sys.exit(-1) 215 | 216 | if errorlog.has_errors: 217 | print("WARNING: Errors encountered.", file=sys.stderr) 218 | sys.exit(-1) 219 | 220 | sys.exit(0) 221 | 222 | 223 | if __name__ == "__main__": 224 | mdimggen_main() 225 | 226 | 227 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 228 | -------------------------------------------------------------------------------- /openscad_docsgen/imagemanager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import print_function 4 | 5 | import os 6 | import re 7 | import sys 8 | import math 9 | import numpy 10 | import filecmp 11 | import os.path 12 | import subprocess 13 | from collections import namedtuple 14 | 15 | from scipy.linalg import norm 16 | from imageio import imread 17 | from PIL import Image, ImageChops 18 | from openscad_runner import RenderMode, OpenScadRunner, ColorScheme 19 | 20 | 21 | class ImageRequest(object): 22 | _size_re = re.compile(r'Size *= *([0-9]+) *x *([0-9]+)') 23 | _frames_re = re.compile(r'Frames *= *([0-9]+)') 24 | _framems_re = re.compile(r'FrameMS *= *([0-9]+)') 25 | _fps_re = re.compile(r'FPS *= *([0-9.]+)') 26 | _vpt_re = re.compile(r'VPT *= *\[([^]]+)\]') 27 | _vpr_re = re.compile(r'VPR *= *\[([^]]+)\]') 28 | _vpd_re = re.compile(r'VPD *= *([a-zA-Z0-9_()+*/$.-]+)') 29 | _vpf_re = re.compile(r'VPF *= *([a-zA-Z0-9_()+*/$.-]+)') 30 | _color_scheme_re = re.compile(r'ColorScheme *= *([a-zA-Z0-9_ ]+)') 31 | 32 | def __init__(self, src_file, src_line, image_file, script_lines, image_meta, starting_cb=None, completion_cb=None, verbose=False, enabled_features=[], default_colorscheme="Cornfield"): 33 | self.src_file = src_file 34 | self.src_line = src_line 35 | self.image_file = image_file 36 | self.image_meta = image_meta 37 | self.enabled_features = enabled_features 38 | self.script_lines = [ 39 | line[2:] if line.startswith("--") else line 40 | for line in script_lines 41 | ] 42 | self.completion_cb = completion_cb 43 | self.starting_cb = starting_cb 44 | self.verbose = verbose 45 | 46 | self.render_mode = RenderMode.preview 47 | self.imgsize = (320, 240) 48 | self.camera = None 49 | self.animation_frames = None 50 | self.frame_ms = 250 51 | self.show_edges = "Edges" in image_meta 52 | self.show_axes = "NoAxes" not in image_meta 53 | self.show_scales = "NoScales" not in image_meta 54 | self.orthographic = "Perspective" not in image_meta 55 | self.script_under = False 56 | self.color_scheme = default_colorscheme 57 | 58 | if "ThrownTogether" in image_meta: 59 | self.render_mode = RenderMode.thrown_together 60 | elif "Render" in image_meta: 61 | self.render_mode = RenderMode.render 62 | 63 | m = self._size_re.search(image_meta) 64 | scale = 1.0 65 | if m: 66 | self.imgsize = (int(m.group(1)), int(m.group(2))) 67 | elif "Small" in image_meta: 68 | scale = 0.75 69 | elif "Med" in image_meta: 70 | scale = 1.5 71 | elif "Big" in image_meta: 72 | scale = 2.0 73 | elif "Huge" in image_meta: 74 | scale = 2.5 75 | self.imgsize = [scale*x for x in self.imgsize] 76 | 77 | vpt = [0, 0, 0] 78 | vpr = [55, 0, 25] 79 | vpd = 444 80 | vpf = 22.5 81 | dynamic_vp = False 82 | 83 | if "3D" in image_meta: 84 | vpr = [55, 0, 25] 85 | elif "2D" in image_meta: 86 | vpr = [0, 0, 0] 87 | if "FlatSpin" in image_meta: 88 | vpr = [55, 0, "360*$t"] 89 | dynamic_vp = True 90 | elif "Spin" in image_meta: 91 | vpr = ["90-45*cos(360*$t)", 0, "360*$t"] 92 | dynamic_vp = True 93 | elif "XSpin" in image_meta: 94 | vpr = ["360*$t", 0, 25] 95 | dynamic_vp = True 96 | elif "YSpin" in image_meta: 97 | vpr = [55, "360*$t", 25] 98 | dynamic_vp = True 99 | if "Anim" in image_meta: 100 | dynamic_vp = True 101 | 102 | match = self._vpr_re.search(image_meta) 103 | if match: 104 | vpr, dyn_vp = self._parse_vp_line(match.group(1), vpr, dynamic_vp) 105 | dynamic_vp = dynamic_vp or dyn_vp 106 | match = self._vpt_re.search(image_meta) 107 | if match: 108 | vpt, dyn_vp = self._parse_vp_line(match.group(1), vpt, dynamic_vp) 109 | dynamic_vp = dynamic_vp or dyn_vp 110 | match = self._vpd_re.search(image_meta) 111 | if match: 112 | vpd = float(match.group(1)) 113 | dynamic_vp = True 114 | match = self._vpf_re.search(image_meta) 115 | if match: 116 | vpf = float(match.group(1)) 117 | dynamic_vp = True 118 | 119 | if dynamic_vp: 120 | self.camera = None 121 | self.script_lines[0:0] = [ 122 | "$vpt = [{}, {}, {}];".format(*vpt), 123 | "$vpr = [{}, {}, {}];".format(*vpr), 124 | "$vpd = {};".format(vpd), 125 | "$vpf = {};".format(vpf), 126 | ] 127 | else: 128 | self.camera = [vpt[0],vpt[1],vpt[2], vpr[0],vpr[1],vpr[2], vpd] 129 | 130 | match = self._fps_re.search(image_meta) 131 | if match: 132 | self.frame_ms = int(1000/float(match.group(1))) 133 | match = self._framems_re.search(image_meta) 134 | if match: 135 | self.frame_ms = int(match.group(1)) 136 | 137 | if "Spin" in image_meta or "Anim" in image_meta: 138 | self.animation_frames = 36 139 | match = self._frames_re.search(image_meta) 140 | if match: 141 | self.animation_frames = int(match.group(1)) 142 | 143 | color_scheme_match = self._color_scheme_re.search(image_meta) 144 | if color_scheme_match: 145 | self.color_scheme = color_scheme_match.group(1) 146 | 147 | longest = max(len(line) for line in self.script_lines) 148 | maxlen = (880 - self.imgsize[0]) / 9 149 | if longest > maxlen or "ScriptUnder" in image_meta: 150 | self.script_under = True 151 | 152 | self.complete = False 153 | self.status = "INCOMPLETE" 154 | self.success = False 155 | self.cmdline = [] 156 | self.return_code = None 157 | self.stdout = [] 158 | self.stderr = [] 159 | self.echos = [] 160 | self.warnings = [] 161 | self.errors = [] 162 | 163 | def _parse_vp_line(self, line, old_trio, dynamic): 164 | comps = line.split(",") 165 | trio = [] 166 | if len(comps) == 3: 167 | for comp in comps: 168 | comp = comp.strip() 169 | try: 170 | trio.append(float(comp)) 171 | except ValueError: 172 | trio.append(comp) 173 | dynamic = True 174 | trio = trio if trio else old_trio 175 | return trio, dynamic 176 | 177 | def starting(self): 178 | if self.starting_cb: 179 | self.starting_cb(self) 180 | 181 | def completed(self, status, osc=None): 182 | self.complete = True 183 | self.status = status 184 | if osc: 185 | self.success = osc.success 186 | self.cmdline = osc.cmdline 187 | self.return_code = osc.return_code 188 | self.stdout = osc.stdout 189 | self.stderr = osc.stderr 190 | self.echos = osc.echos 191 | self.warnings = osc.warnings 192 | self.errors = osc.errors 193 | else: 194 | self.success = True 195 | if self.completion_cb: 196 | self.completion_cb(self) 197 | 198 | 199 | class ImageManager(object): 200 | 201 | def __init__(self): 202 | self.requests = [] 203 | self.test_only = False 204 | 205 | def purge_requests(self): 206 | self.requests = [] 207 | 208 | def new_request(self, src_file, src_line, image_file, script_lines, image_meta, starting_cb=None, completion_cb=None, verbose=False, enabled_features=[], default_colorscheme="Cornfield"): 209 | if "NORENDER" in image_meta: 210 | raise Exception("Cannot render scripts marked NORENDER") 211 | req = ImageRequest(src_file, src_line, image_file, script_lines, image_meta, starting_cb, completion_cb, verbose=verbose, enabled_features=enabled_features, default_colorscheme=default_colorscheme) 212 | self.requests.append(req) 213 | return req 214 | 215 | def process_requests(self, test_only=False): 216 | self.test_only = test_only 217 | for req in self.requests: 218 | self.process_request(req) 219 | self.requests = [] 220 | 221 | def process_request(self, req): 222 | req.starting() 223 | 224 | dir_name = os.path.dirname(req.image_file) 225 | base_name = os.path.basename(req.image_file) 226 | file_base, file_ext = os.path.splitext(base_name) 227 | script_file = "tmp_{0}.scad".format(base_name.replace(".", "_")) 228 | targ_img_file = req.image_file 229 | new_img_file = "tmp_{0}{1}".format(file_base, file_ext) 230 | 231 | with open(script_file, "w") as f: 232 | for line in req.script_lines: 233 | f.write(line + "\n") 234 | 235 | try: 236 | no_vp = True 237 | for line in req.script_lines: 238 | if "$vp" in line: 239 | no_vp = False 240 | 241 | render_mode = req.render_mode 242 | animate = req.animation_frames 243 | if self.test_only: 244 | render_mode = RenderMode.test_only 245 | animate = None 246 | 247 | osc = OpenScadRunner( 248 | script_file, 249 | new_img_file, 250 | animate=animate, 251 | animate_duration=req.frame_ms, 252 | imgsize=req.imgsize, 253 | antialias=2, 254 | orthographic=True, 255 | camera=req.camera, 256 | auto_center=no_vp, 257 | view_all=no_vp, 258 | color_scheme = req.color_scheme, 259 | show_edges=req.show_edges, 260 | show_axes=req.show_axes, 261 | show_scales=req.show_scales, 262 | render_mode=render_mode, 263 | hard_warnings=no_vp, 264 | verbose=req.verbose, 265 | enabled=req.enabled_features, 266 | ) 267 | osc.run() 268 | masked_warnings = [ 269 | "Viewall and autocenter disabled", 270 | "failed with error, falling back to Nef operation", 271 | ] 272 | warnings = [] 273 | for line in osc.warnings: 274 | is_masked = False 275 | for mask in masked_warnings: 276 | if mask in line: 277 | is_masked = True 278 | if not is_masked: 279 | warnings.append(line) 280 | osc.warnings = warnings 281 | 282 | finally: 283 | os.unlink(script_file) 284 | 285 | if not osc.good() or osc.warnings or osc.errors: 286 | osc.success = False 287 | req.completed("FAIL", osc) 288 | return 289 | 290 | if self.test_only: 291 | req.completed("SKIP", osc) 292 | return 293 | 294 | os.makedirs(os.path.dirname(targ_img_file), exist_ok=True) 295 | 296 | # Time to compare image. 297 | if not os.path.isfile(targ_img_file): 298 | os.rename(new_img_file, targ_img_file) 299 | req.completed("NEW", osc) 300 | elif self.image_compare(targ_img_file, new_img_file): 301 | os.unlink(new_img_file) 302 | req.completed("SKIP", osc) 303 | else: 304 | os.unlink(targ_img_file) 305 | os.rename(new_img_file, targ_img_file) 306 | req.completed("REPLACE", osc) 307 | 308 | @staticmethod 309 | def image_compare(file1, file2, max_diff=64.0): 310 | """ 311 | Compare two image files. Returns true if they are almost exactly the same. 312 | """ 313 | if file1.endswith(".gif") and file2.endswith(".gif"): 314 | return filecmp.cmp(file1, file2, shallow=False) 315 | else: 316 | img1 = imread(file1).astype(float) 317 | img2 = imread(file2).astype(float) 318 | if img1.shape != img2.shape: 319 | return False 320 | # calculate the difference and its norms 321 | diff = img1 - img2 # elementwise for scipy arrays 322 | diff_max = numpy.max(abs(diff)) 323 | return diff_max <= max_diff 324 | 325 | 326 | image_manager = ImageManager() 327 | 328 | 329 | 330 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 331 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ################################ 2 | OpenSCAD Documentation Generator 3 | ################################ 4 | 5 | This package generates wiki-ready GitHub flavored markdown documentation pages from in-line source 6 | code comments. This is similar to Doxygen or JavaDoc, but designed for use with OpenSCAD code. 7 | Example images can be generated automatically from short example scripts. 8 | 9 | Documentation about how to add documentation comments to OpenSCAD code can be found at 10 | `https://github.com/revarbat/openscad_docsgen/blob/main/WRITING_DOCS.md` 11 | 12 | 13 | Installing openscad-docsgen 14 | --------------------------- 15 | 16 | The easiest way to install this is to use pip:: 17 | 18 | % pip3 install openscad_docsgen 19 | 20 | To install directly from these sources, you can instead do:: 21 | 22 | % python3 setup.py build install 23 | 24 | 25 | Using openscad-docsgen 26 | ---------------------- 27 | 28 | The simplest way to generate documentation is:: 29 | 30 | % openscad-docsgen -m *.scad 31 | 32 | Which will read all of .scad files in the current directory, and writes out documentation 33 | for each .scad file to the ``./docs/`` dir. To write out to a different directory, use 34 | the ``-D`` argument:: 35 | 36 | % openscad-docsgen -D wikidir -m *.scad 37 | 38 | To write out an alphabetical function/module index markdown file, use the ``-i`` flag:: 39 | 40 | % openscad-docsgen -i *.scad 41 | 42 | To write out a Table of Contents markdown file, use the ``-t`` flag:: 43 | 44 | % openscad-docsgen -t *.scad 45 | 46 | To write out a CheatSheet markdown file, use the ``-c`` flag. In addition, you can 47 | specify the project name shown in the CheatSheet with the ``-P PROJECTNAME`` argument:: 48 | 49 | % openscad-docsgen -c -P "My Foobar Library" *.scad 50 | 51 | A Topics index file can be generated by passing the ``-I`` flag:: 52 | 53 | % openscad-docsgen -I *.scad 54 | 55 | You can just test for script errors more quickly with the ``-T`` flag (for test-only):: 56 | 57 | % openscad-docsgen -m -T *.scad 58 | 59 | By default, the target output profile is to generate documentation for a GitHub Wiki. 60 | You can output for a more generic Wiki with ``-p wiki``:: 61 | 62 | % openscad-docsgen -ticmI -p wiki *.scad 63 | 64 | 65 | Docsgen Configuration File 66 | -------------------------- 67 | You can also make more persistent configurations by putting a `.openscad_docsgen_rc` file in the 68 | directory you will be running openscad-docsgen from. It can look something like this:: 69 | 70 | DocsDirectory: WikiDir/ 71 | TargetProfile: githubwiki 72 | ProjectName: The Foobar Project 73 | GeneratedDocs: Files, ToC, Index, Topics, CheatSheet 74 | SidebarHeader: 75 | ## Indices 76 | . 77 | SidebarMiddle: 78 | [Tutorials](Tutorials) 79 | IgnoreFiles: 80 | foo.scad 81 | std.scad 82 | version.scad 83 | tmp_*.scad 84 | PrioritizeFiles: 85 | First.scad 86 | Second.scad 87 | Third.scad 88 | Fourth.scad 89 | DefineHeader(BulletList): Side Effects 90 | DefineHeader(Table;Headers=Anchor Name|Position): Extra Anchors 91 | 92 | For an explanation of the syntax and the specific headers, see: 93 | `https://github.com/revarbat/openscad_docsgen/blob/main/WRITING_DOCS.md` 94 | 95 | Using openscad-mdimggen 96 | ----------------------- 97 | If you have MarkDown based files that you would like to generate images for, you can use the 98 | `openscad_mdimggen` command. It can take the following arguments:: 99 | 100 | -h, --help Show help message and exit 101 | -D DOCS_DIR, --docs-dir DOCS_DIR 102 | The directory to put generated documentation in. 103 | -P FILE_PREFIX, --file-prefix FILE_PREFIX 104 | The prefix to put in front of each output markdown file. 105 | -T, --test-only If given, don't generate images, but do try executing the scripts. 106 | -I IMAGE_ROOT, --image_root IMAGE_ROOT 107 | The directory to put generated images in. 108 | -f, --force If given, force regeneration of images. 109 | -a, --png-animation If given, animations are created using animated PNGs instead of GIFs. 110 | 111 | What `openscad-mdimggen` will do is read the input MarkDown file and look for fenced scripts of 112 | OpenSCAD code, that starts with a line of the form:: 113 | 114 | ```openscad-METADATA 115 | 116 | It will copy all non-script lines to the output markdown file, and run OpenSCAD for each of the 117 | found fenced scripts, inserting the generated image into the output MarkDown file after the script block. 118 | The METADATA for each script will define the viewpoint and other info for the given generated image. 119 | This METADATA takes the form of a set of semi-colon separated options that can be any of the following: 120 | 121 | - ``NORENDER``: Don't generate an image for this example, but show the example text. 122 | - ``ImgOnly``: Generate and show the image, but hide the text of the script. 123 | - ``Hide``: Generate, but don't show script or image. This can be used to generate images to be manually displayed in markdown text blocks. 124 | - ``2D``: Orient camera in a top-down view for showing 2D objects. 125 | - ``3D``: Orient camera in an oblique view for showing 3D objects. 126 | - ``VPT=[10,20,30]`` Force the viewpoint translation `$vpt` to `[10,20,30]`. 127 | - ``VPR=[55,0,600]`` Force the viewpoint rotation `$vpr` to `[55,0,60]`. 128 | - ``VPD=440``: Force viewpoint distance `$vpd` to 440. 129 | - ``VPF=22.5``: Force field of view angle `$vpf` to 22.5. 130 | - ``Spin``: Animate camera orbit around the `[0,1,1]` axis to display all sides of an object. 131 | - ``FlatSpin``: Animate camera orbit around the Z axis, above the XY plane. 132 | - ``Anim``: Make an animation where `$t` varies from `0.0` to almost `1.0`. 133 | - ``FrameMS=250``: Sets the number of milliseconds per frame for spins and animation. 134 | - ``FPS=8``: Sets the number of frames per second for spins and animation. 135 | - ``Frames=36``: Number of animation frames to make. 136 | - ``Small``: Make the image small sized. 137 | - ``Med``: Make the image medium sized. 138 | - ``Big``: Make the image big sized. 139 | - ``Huge``: Make the image huge sized. 140 | - ``Size=880x640``: Make the image 880 by 640 pixels in size. 141 | - ``Render``: Force full rendering from OpenSCAD, instead of the normal preview. 142 | - ``Edges``: Highlight face edges. 143 | - ``NoAxes``: Hides the axes and scales. 144 | - ``NoScales``: Hides the scale numbers along the axes. 145 | - ``ColorScheme``: Generate the image using a specific color scheme 146 | - Usage: ``ColorScheme=`` (e.g. ``ColorScheme=BeforeDawn``) 147 | - Default color scheme: ``Cornfield`` 148 | - Predefined color schemes: ``Cornfield``, ``Metallic``, ``Sunset``, ``Starnight``, ``BeforeDawn``, ``Nature``, ``DeepOcean``, ``Solarized``, ``Tomorrow``, ``Tomorrow Night``, ``Monotone`` 149 | - Color schemes defined as a [Read-only Resource](https://github.com/openscad/openscad/wiki/Path-locations#read-only-resources) or [User Resource](https://github.com/openscad/openscad/wiki/Path-locations#user-resources) are also supported. 150 | 151 | For example:: 152 | 153 | ```openscad-FlatSpin;VPD=500 154 | prismoid([60,40], [40,20], h=40, offset=[10,10]); 155 | ``` 156 | 157 | Will generate an animated flat spin of the prismoid at a viewing distance of 500. While:: 158 | 159 | ```openscad-3D;Big 160 | prismoid([60,40], [40,20], h=40, offset=[10,10]); 161 | ``` 162 | 163 | Will generate a still image of the same prismoid, but at a bigger image size. 164 | 165 | MDImgGen Configuration File 166 | --------------------------- 167 | You can store defaults for ``openscad_mdimggen`` in the ``.openscad_mdimggen_rc`` file like this:: 168 | 169 | docs_dir: "BOSL2.wiki" 170 | image_root: "images/tutorials" 171 | file_prefix: "Tutorial-" 172 | source_files: "tutorials/*.md" 173 | png_animations: true 174 | 175 | 176 | External Calling 177 | ---------------- 178 | Here's an example of how to use this library, to get the parsed documentation data:: 179 | 180 | import openscad_docsgen as docsgen 181 | from glob import glob 182 | from pprint import pprint 183 | dgp = docsgen.DocsGenParser(quiet=True) 184 | dgp.parse_files(glob("*.scad")) 185 | for name in dgp.get_indexed_names(): 186 | data = dgp.get_indexed_data(name) 187 | pprint(name) 188 | pprint(data["description"]) 189 | 190 | The data for an OpenSCAD function, module, or constant generally looks like:: 191 | 192 | { 193 | 'name': 'Function&Module', // Could also be 'Function', 'Module', or 'Constant' 194 | 'subtitle': 'line_of()', 195 | 'body': [], 196 | 'file': 'distributors.scad', 197 | 'line': 43, 198 | 'aliases': ['linear_spread()'], 199 | 'topics': ['Distributors'], 200 | 'usages': [ 201 | { 202 | 'subtitle': 'Spread `n` copies by a given spacing', 203 | 'body': ['line_of(spacing, , ) ...'] 204 | }, 205 | { 206 | 'subtitle': 'Spread copies every given spacing along the line', 207 | 'body': ['line_of(spacing, , ) ...'] 208 | }, 209 | { 210 | 'subtitle': 'Spread `n` copies along the length of the line', 211 | 'body': ['line_of(, , ) ...'] 212 | }, 213 | { 214 | 'subtitle': 'Spread `n` copies along the line from `p1` to `p2`', 215 | 'body': ['line_of(, , ) ...'] 216 | }, 217 | { 218 | 'subtitle': 'Spread copies every given spacing, centered along the line from `p1` to `p2`', 219 | 'body': ['line_of(, , ) ...'] 220 | }, 221 | { 222 | 'subtitle': 'As a function', 223 | 'body': [ 224 | 'pts = line_of(, , );', 225 | 'pts = line_of(, , );', 226 | 'pts = line_of(, , );', 227 | 'pts = line_of(, , );', 228 | 'pts = line_of(, , );' 229 | ] 230 | } 231 | ], 232 | 'description': [ 233 | 'When called as a function, returns a list of points at evenly spread positions along a line.', 234 | 'When called as a module, copies `children()` at one or more evenly spread positions along a line.', 235 | 'By default, the line will be centered at the origin, unless the starting point `p1` is given.', 236 | 'The line will be pointed towards `RIGHT` (X+) unless otherwise given as a vector in `l`,', 237 | '`spacing`, or `p1`/`p2`.', 238 | ], 239 | 'arguments': [ 240 | 'spacing = The vector giving both the direction and spacing distance between each set of copies.', 241 | 'n = Number of copies to distribute along the line. (Default: 2)', 242 | '---', 243 | 'l = Either the scalar length of the line, or a vector giving both the direction and length of the line.', 244 | 'p1 = If given, specifies the starting point of the line.', 245 | 'p2 = If given with `p1`, specifies the ending point of line, and indirectly calculates the line length.' 246 | ], 247 | 'see_also': ['xcopies()', 'ycopies()'], 248 | 'examples': [ 249 | ['line_of(10) sphere(d=1);'], 250 | ['line_of(10, n=5) sphere(d=1);'], 251 | ['line_of([10,5], n=5) sphere(d=1);'], 252 | ['line_of(spacing=10, n=6) sphere(d=1);'], 253 | ['line_of(spacing=[10,5], n=6) sphere(d=1);'], 254 | ['line_of(spacing=10, l=50) sphere(d=1);'], 255 | ['line_of(spacing=10, l=[50,30]) sphere(d=1);'], 256 | ['line_of(spacing=[10,5], l=50) sphere(d=1);'], 257 | ['line_of(l=50, n=4) sphere(d=1);'], 258 | ['line_of(l=[50,-30], n=4) sphere(d=1);'], 259 | [ 260 | 'line_of(p1=[0,0,0], p2=[5,5,20], n=6) ' 261 | 'cube(size=[3,2,1],center=true);' 262 | ], 263 | [ 264 | 'line_of(p1=[0,0,0], p2=[5,5,20], spacing=6) ' 265 | 'cube(size=[3,2,1],center=true);' 266 | ], 267 | [ 268 | 'line_of(l=20, n=3) {', 269 | ' cube(size=[1,3,1],center=true);', 270 | ' cube(size=[3,1,1],center=true);', 271 | '}' 272 | ], 273 | [ 274 | 'pts = line_of([10,5],n=5);', 275 | 'move_copies(pts) circle(d=2);' 276 | ] 277 | ], 278 | 'children': [ 279 | { 280 | 'name': 'Side Effects', 281 | 'subtitle': '', 282 | 'body': [ 283 | '`$pos` is set to the relative centerpoint of each child copy.', 284 | '`$idx` is set to the index number of each child being copied.' 285 | ], 286 | 'file': 'distributors.scad', 287 | 'line': 88 288 | } 289 | ] 290 | } 291 | 292 | 293 | -------------------------------------------------------------------------------- /openscad_docsgen/blocks.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | import os.path 5 | import re 6 | import sys 7 | 8 | from .utils import flatten 9 | from .errorlog import ErrorLog, errorlog 10 | from .imagemanager import image_manager 11 | from .logmanager import log_manager 12 | 13 | 14 | class DocsGenException(Exception): 15 | def __init__(self, block="", message=""): 16 | self.block = block 17 | self.message = message 18 | super().__init__('{} "{}"'.format(self.message, self.block)) 19 | 20 | 21 | class GenericBlock(object): 22 | _link_pat = re.compile(r'^(.*?)\{\{([A-Za-z0-9_()]+)\}\}(.*)$') 23 | 24 | def __init__(self, title, subtitle, body, origin, parent=None): 25 | self.title = title 26 | self.subtitle = subtitle 27 | self.body = body 28 | self.origin = origin 29 | self.parent = parent 30 | self.children = [] 31 | self.figure_num = 0 32 | self.definitions = {} 33 | if parent: 34 | parent.children.append(self) 35 | 36 | def __str__(self): 37 | return "{}: {}".format( 38 | self.title.replace('&', '/'), 39 | self.subtitle 40 | ) 41 | 42 | def get_figure_num(self): 43 | if self.parent: 44 | return self.parent.get_figure_num() 45 | return "" 46 | 47 | def sort_children(self, front_blocks=(), back_blocks=()): 48 | children = [] 49 | for blocks in front_blocks: 50 | for block in blocks: 51 | for child in self.children: 52 | if child.title.startswith(block): 53 | children.append(child) 54 | blocks = flatten(front_blocks + back_blocks) 55 | for child in self.children: 56 | found = [block for block in blocks if child.title.startswith(block)] 57 | if not found: 58 | children.append(child) 59 | for blocks in back_blocks: 60 | for block in blocks: 61 | for child in self.children: 62 | if child.title.startswith(block): 63 | children.append(child) 64 | return children 65 | 66 | def get_children_by_title(self, titles): 67 | if isinstance(titles,str): 68 | titles = [titles] 69 | return [ 70 | child 71 | for child in self.children 72 | if child.title in titles 73 | ] 74 | 75 | def get_data(self): 76 | d = { 77 | "name": self.title, 78 | "subtitle": self.subtitle, 79 | "body": self.body, 80 | "file": self.origin.file, 81 | "line": self.origin.line 82 | } 83 | if self.children: 84 | d["children"] = [child.get_data() for child in self.children] 85 | return d 86 | 87 | def get_link(self, target, currfile=None, literalize=False, html=False): 88 | return self.title 89 | 90 | def parse_links(self, line, controller, target, html=False): 91 | oline = "" 92 | while line: 93 | m = self._link_pat.match(line) 94 | if m: 95 | oline += m.group(1) 96 | name = m.group(2).lower().strip() 97 | line = m.group(3) 98 | literalize = name.endswith("()") 99 | if name in controller.items_by_name: 100 | item = controller.items_by_name[name] 101 | oline += item.get_link(target, currfile=self.origin.file, literalize=literalize, html=html) 102 | elif name in controller.definitions: 103 | oline += target.get_link(name, anchor=name.lower(), file="Glossary", literalize=literalize, html=html) 104 | elif name in controller.defn_aliases: 105 | term = controller.defn_aliases[name] 106 | oline += target.get_link(name, anchor=term.lower(), file="Glossary", literalize=literalize, html=html) 107 | else: 108 | print(controller.definitions) 109 | msg = "Invalid Link {{{{{0}}}}}".format(name) 110 | errorlog.add_entry(self.origin.file, self.origin.line, msg, ErrorLog.FAIL) 111 | oline += name 112 | else: 113 | oline += line 114 | line = "" 115 | return oline 116 | 117 | def get_markdown_body(self, controller, target): 118 | out = [] 119 | if not self.body: 120 | return out 121 | in_block = False 122 | for line in self.body: 123 | if line.startswith("```"): 124 | in_block = not in_block 125 | if in_block or line.startswith(" "): 126 | out.append(line) 127 | elif line == ".": 128 | out.append("") 129 | else: 130 | out.append(self.parse_links(line, controller, target)) 131 | return out 132 | 133 | def get_tocfile_lines(self, controller, target, n=1, currfile=""): 134 | return [] 135 | 136 | def get_toc_lines(self, controller, target, n=1, currfile=""): 137 | return [] 138 | 139 | def get_cheatsheet_lines(self, controller, target): 140 | return [] 141 | 142 | def get_file_lines(self, controller, target): 143 | sub = self.parse_links(self.subtitle, controller, target) 144 | out = target.block_header(self.title, sub) 145 | out.extend(self.get_markdown_body(controller, target)) 146 | out.append("") 147 | return out 148 | 149 | def __eq__(self, other): 150 | return self.title == other.title and self.subtitle == other.subtitle 151 | 152 | def __lt__(self, other): 153 | if self.subtitle == other.subtitle: 154 | return self.title < other.title 155 | return self.subtitle < other.subtitle 156 | 157 | 158 | class LabelBlock(GenericBlock): 159 | def __init__(self, title, subtitle, body, origin, parent=None): 160 | if body: 161 | raise DocsGenException(title, "Body not supported, while declaring block:") 162 | super().__init__(title, subtitle, body, origin, parent=parent) 163 | 164 | 165 | class SynopsisBlock(LabelBlock): 166 | def __init__(self, title, subtitle, body, origin, parent=None): 167 | parent.synopsis = subtitle 168 | super().__init__(title, subtitle, body, origin, parent=parent) 169 | 170 | def get_file_lines(self, controller, target): 171 | sub = self.parse_links(self.subtitle, controller, target) 172 | sub = target.escape_entities(sub) + target.mouseover_tags(self.parent.syntags, htag="sup", wrap="[{}]") 173 | out = target.block_header(self.title, sub, escsub=False) 174 | return out 175 | 176 | 177 | class SynTagsBlock(LabelBlock): 178 | def __init__(self, title, subtitle, body, origin, parent, syntags_data={}): 179 | tags = [x.strip() for x in subtitle.split(",")] 180 | for tag in tags: 181 | parent.syntags[tag] = syntags_data[tag] 182 | super().__init__(title, subtitle, body, origin, parent=parent) 183 | 184 | def get_file_lines(self, controller, target): 185 | return [] 186 | 187 | 188 | class TopicsBlock(LabelBlock): 189 | def __init__(self, title, subtitle, body, origin, parent=None): 190 | super().__init__(title, subtitle, body, origin, parent=parent) 191 | self.topics = [x.strip() for x in subtitle.split(",")] 192 | parent.topics = self.topics 193 | 194 | def get_file_lines(self, controller, target): 195 | links = [ 196 | target.get_link(topic, anchor=target.header_link(topic), file="Topics", literalize=False) 197 | for topic in self.topics 198 | ] 199 | links = ", ".join(links) 200 | out = target.block_header(self.title, links) 201 | return out 202 | 203 | 204 | class SeeAlsoBlock(LabelBlock): 205 | def __init__(self, title, subtitle, body, origin, parent=None): 206 | self.see_also = [x.strip() for x in subtitle.split(",")] 207 | parent.see_also = self.see_also 208 | super().__init__(title, subtitle, body, origin, parent=parent) 209 | 210 | def get_file_lines(self, controller, target): 211 | items = [] 212 | for name in self.see_also: 213 | if name not in controller.items_by_name: 214 | msg = "Invalid Link '{0}'".format(name) 215 | errorlog.add_entry(self.origin.file, self.origin.line, msg, ErrorLog.FAIL) 216 | else: 217 | item = controller.items_by_name[name] 218 | if item is not self.parent: 219 | items.append( item ) 220 | links = [ 221 | item.get_link(target, currfile=self.origin.file, literalize=False) 222 | for item in items 223 | ] 224 | out = [] 225 | links = ", ".join(links) 226 | out.extend(target.block_header(self.title, links, escsub=False)) 227 | return out 228 | 229 | 230 | class HeaderlessBlock(GenericBlock): 231 | def __init__(self, title, subtitle, body, origin, parent=None): 232 | if subtitle: 233 | body.insert(0, subtitle) 234 | subtitle = "" 235 | super().__init__(title, subtitle, body, origin, parent=parent) 236 | 237 | def get_file_lines(self, controller, target): 238 | out = [] 239 | out.append("") 240 | out.extend(self.get_markdown_body(controller, target)) 241 | out.append("") 242 | return out 243 | 244 | 245 | class TextBlock(GenericBlock): 246 | def __init__(self, title, subtitle, body, origin, parent=None): 247 | if subtitle: 248 | body.insert(0, subtitle) 249 | subtitle = "" 250 | super().__init__(title, subtitle, body, origin, parent=parent) 251 | 252 | 253 | class DefinitionsBlock(GenericBlock): 254 | def __init__(self, title, subtitle, body, origin, parent=None): 255 | super().__init__(title, subtitle, body, origin, parent=parent) 256 | terms = [] 257 | self.definitions = {} 258 | for line in body: 259 | if '=' not in line: 260 | raise DocsGenException(title, "Expected body line in the format TERM = DEFINITION, while declaring block:") 261 | termset, defn = line.split('=', 1) 262 | termset = [x.strip() for x in termset.lower().split('|')] 263 | defn = defn.strip() 264 | for term in termset: 265 | if term in terms: 266 | raise DocsGenException(title, "Redefined term '{}', while declaring block:".format(term)) 267 | terms.append(term) 268 | term = termset[0] 269 | self.definitions[term] = (termset, defn) 270 | 271 | def get_file_lines(self, controller, target): 272 | out = target.block_header(self.title, self.subtitle) 273 | terms = list(self.definitions.keys()) 274 | defs = { 275 | key: self.parse_links(info[1], controller, target, html=True) 276 | for key, info in self.definitions.items() 277 | } 278 | for term in terms: 279 | out.extend(target.markdown_block(["{}: {}".format(term.title(), defs[term])])) 280 | out.append("") 281 | return out 282 | 283 | 284 | class BulletListBlock(GenericBlock): 285 | def __init__(self, title, subtitle, body, origin, parent=None): 286 | super().__init__(title, subtitle, body, origin, parent=parent) 287 | 288 | def get_file_lines(self, controller, target): 289 | sub = self.parse_links(self.subtitle, controller, target) 290 | sub = target.escape_entities(sub) 291 | out = target.block_header(self.title, sub) 292 | out.extend(target.bullet_list(self.body)) 293 | return out 294 | 295 | 296 | class NumberedListBlock(GenericBlock): 297 | def __init__(self, title, subtitle, body, origin, parent=None): 298 | super().__init__(title, subtitle, body, origin, parent=parent) 299 | 300 | def get_file_lines(self, controller, target): 301 | sub = self.parse_links(self.subtitle, controller, target) 302 | sub = target.escape_entities(sub) 303 | out = target.block_header(self.title, sub) 304 | out.extend(target.numbered_list(self.body)) 305 | return out 306 | 307 | 308 | class TableBlock(GenericBlock): 309 | def __init__(self, title, subtitle, body, origin, parent=None, header_sets=None): 310 | super().__init__(title, subtitle, body, origin, parent=parent) 311 | self.header_sets = header_sets 312 | tnum = 0 313 | for line in self.body: 314 | if line == "---": 315 | tnum += 1 316 | continue 317 | if tnum >= len(self.header_sets): 318 | raise DocsGenException(title, "More tables than header_sets, while declaring block:") 319 | 320 | def get_file_lines(self, controller, target): 321 | tnum = 0 322 | table = [] 323 | tables = [] 324 | for line in self.body: 325 | if line == "---": 326 | tnum += 1 327 | if table: 328 | tables.append(table) 329 | table = [] 330 | continue 331 | hdr_set = self.header_sets[tnum] 332 | cells = [ 333 | self.parse_links(x.strip(), controller, target) 334 | for x in line.split("=",len(hdr_set)-1) 335 | ] 336 | table.append(cells) 337 | if table: 338 | tables.append(table) 339 | sub = self.parse_links(self.subtitle, controller, target) 340 | sub = target.escape_entities(sub) 341 | out = target.block_header(self.title, sub) 342 | for tnum, table in enumerate(tables): 343 | headers = self.header_sets[tnum] 344 | out.extend(target.table(headers,table)) 345 | return out 346 | 347 | 348 | class FileBlock(GenericBlock): 349 | def __init__(self, title, subtitle, body, origin): 350 | super().__init__(title, subtitle, body, origin) 351 | self.includes = [] 352 | self.common_code = [] 353 | self.footnotes = [] 354 | self.summary = "" 355 | self.group = "" 356 | 357 | def get_data(self): 358 | d = super().get_data() 359 | d["includes"] = self.includes 360 | d["commoncode"] = self.common_code 361 | d["group"] = self.group 362 | d["summary"] = self.summary 363 | d["footnotes"] = [ 364 | { 365 | "mark": mark, 366 | "note": note 367 | } for mark, note in self.footnotes 368 | ] 369 | skip_titles = ["CommonCode", "Includes"] 370 | d["children"] = list(filter(lambda x: x["name"] not in skip_titles, d["children"])) 371 | return d 372 | 373 | def get_link(self, target, currfile=None, label="", literalize=False, html=False): 374 | file = self.origin.file 375 | if currfile is None or self.origin.file == currfile: 376 | file = "" 377 | return target.get_link( 378 | label=label if label else str(self), 379 | anchor="", 380 | file=file, 381 | literalize=literalize, 382 | html=html 383 | ) 384 | 385 | def get_tocfile_lines(self, controller, target, n=1, currfile=""): 386 | sections = [ 387 | sect for sect in self.children 388 | if isinstance(sect, SectionBlock) 389 | ] 390 | link = self.get_link(target, label=self.subtitle, currfile=currfile) 391 | out = [] 392 | out.extend(target.header("{}. {}".format(n, link), lev=target.SECTION, esc=False)) 393 | if self.summary: 394 | out.extend(target.line_with_break(self.summary)) 395 | if self.footnotes: 396 | for mark, note, origin in self.footnotes: 397 | out.extend(target.line_with_break(target.italics(note))) 398 | out.extend(target.bullet_list_start()) 399 | for n, sect in enumerate(sections): 400 | out.extend(sect.get_tocfile_lines(controller, target, n=n+1, currfile=currfile)) 401 | out.extend(target.bullet_list_end()) 402 | return out 403 | 404 | def get_toc_lines(self, controller, target, n=1, currfile=""): 405 | sections = [ 406 | sect for sect in self.children 407 | if isinstance(sect, SectionBlock) 408 | ] 409 | out = [] 410 | out.extend(target.numbered_list_start()) 411 | for n, sect in enumerate(sections): 412 | out.extend(sect.get_toc_lines(controller, target, n=n+1, currfile=currfile)) 413 | out.extend(target.numbered_list_end()) 414 | return out 415 | 416 | def get_cheatsheet_lines(self, controller, target): 417 | lines = [] 418 | for child in self.get_children_by_title("Section"): 419 | lines.extend(child.get_cheatsheet_lines(controller, target)) 420 | out = [] 421 | if lines: 422 | out.extend(target.header("{}: {}".format(self.title, self.subtitle), lev=target.SUBSECTION)) 423 | out.extend(lines) 424 | return out 425 | 426 | def get_file_lines(self, controller, target): 427 | out = target.header(str(self), lev=target.FILE) 428 | out.extend(target.markdown_block(self.get_markdown_body(controller, target))) 429 | for child in self.children: 430 | if not isinstance(child, SectionBlock): 431 | out.extend(child.get_file_lines(controller, target)) 432 | out.extend(target.header("File Contents", lev=target.SECTION)) 433 | out.extend(self.get_toc_lines(controller, target, currfile=self.origin.file)) 434 | for child in self.children: 435 | if isinstance(child, SectionBlock): 436 | out.extend(child.get_file_lines(controller, target)) 437 | return out 438 | 439 | def get_figure_num(self): 440 | return "{}".format(self.figure_num) 441 | 442 | 443 | class IncludesBlock(GenericBlock): 444 | def __init__(self, title, subtitle, body, origin, parent=None): 445 | super().__init__(title, subtitle, body, origin, parent=parent) 446 | if parent: 447 | parent.includes.extend(body) 448 | 449 | def get_file_lines(self, controller, target): 450 | out = [] 451 | if self.body: 452 | out.extend(target.markdown_block([ 453 | "To use, add the following lines to the beginning of your file:" 454 | ])) 455 | out.extend(target.markdown_block(target.indent_lines(self.body))) 456 | return out 457 | 458 | 459 | class SectionBlock(GenericBlock): 460 | def __init__(self, title, subtitle, body, origin, parent=None): 461 | super().__init__(title, subtitle, body, origin, parent=parent) 462 | if parent: 463 | self.parent.figure_num += 1 464 | 465 | def get_link(self, target, currfile=None, label="", literalize=False, html=False): 466 | file = self.origin.file 467 | if currfile is None or self.origin.file == currfile: 468 | file = "" 469 | return target.get_link( 470 | label=label if label else str(self), 471 | anchor=target.header_link(str(self)), 472 | file=file, 473 | literalize=literalize, 474 | html=html 475 | ) 476 | 477 | def get_tocfile_lines(self, controller, target, n=1, currfile=""): 478 | """ 479 | Return the markdown table of contents lines for the children in this 480 | section. This is returned as a series of bullet points. 481 | `indent` sets the level of indentation for the bullet points 482 | """ 483 | out = [] 484 | if self.subtitle: 485 | item = self.get_link(target, label=self.subtitle, currfile=currfile) 486 | out.extend(target.line_with_break(target.bullet_list_item(item))) 487 | subsects = self.get_children_by_title("Subsection") 488 | if subsects: 489 | out.extend(target.bullet_list_start()) 490 | for child in subsects: 491 | out.extend(target.indent_lines(child.get_tocfile_lines(controller, target, currfile=currfile))) 492 | out.extend(target.bullet_list_end()) 493 | out.extend( 494 | target.indent_lines( 495 | target.bullet_list( 496 | flatten([ 497 | child.get_tocfile_lines(controller, target, currfile=currfile) 498 | for child in self.get_children_by_title( 499 | ["Constant","Function","Module","Function&Module"] 500 | ) 501 | ]) 502 | ) 503 | ) 504 | ) 505 | else: 506 | for child in self.get_children_by_title("Subsection"): 507 | out.extend(child.get_tocfile_lines(controller, target, currfile=currfile)) 508 | out.extend( 509 | target.indent_lines( 510 | target.bullet_list( 511 | flatten([ 512 | child.get_tocfile_lines(controller, target, currfile=currfile) 513 | for child in self.get_children_by_title( 514 | ["Constant","Function","Module","Function&Module"] 515 | ) 516 | ]) 517 | ) 518 | ) 519 | ) 520 | return out 521 | 522 | def get_toc_lines(self, controller, target, n=1, currfile=""): 523 | """ 524 | Return the markdown table of contents lines for the children in this 525 | section. This is returned as a series of bullet points. 526 | `indent` sets the level of indentation for the bullet points 527 | """ 528 | lines = [] 529 | subsects = self.get_children_by_title("Subsection") 530 | if subsects: 531 | lines.extend(target.numbered_list_start()) 532 | for num, child in enumerate(subsects): 533 | lines.extend(child.get_toc_lines(controller, target, currfile=currfile, n=num+1)) 534 | lines.extend(target.numbered_list_end()) 535 | for child in self.get_children_by_title(["Constant","Function","Module","Function&Module"]): 536 | lines.extend(child.get_toc_lines(controller, target, currfile=currfile)) 537 | out = [] 538 | if self.subtitle: 539 | item = self.get_link(target, currfile=currfile) 540 | out.extend(target.numbered_list_item(n, item)) 541 | out.extend(target.bullet_list_start()) 542 | out.extend(target.indent_lines(lines)) 543 | out.extend(target.bullet_list_end()) 544 | else: 545 | out.extend(target.bullet_list_start()) 546 | out.extend(lines) 547 | out.extend(target.bullet_list_end()) 548 | return out 549 | 550 | def get_cheatsheet_lines(self, controller, target): 551 | subs = [] 552 | for child in self.get_children_by_title("Subsection"): 553 | subs.extend(child.get_cheatsheet_lines(controller, target)) 554 | consts = [] 555 | for cnst in self.get_children_by_title("Constant"): 556 | consts.append(cnst.get_link(target, currfile="CheatSheet")) 557 | for alias in cnst.aliases: 558 | consts.append(cnst.get_link(target, label=alias, currfile="CheatSheet")) 559 | items = [] 560 | for child in self.get_children_by_title(["Function","Module","Function&Module"]): 561 | items.extend(child.get_cheatsheet_lines(controller, target)) 562 | out = [] 563 | if subs or consts or items: 564 | out.extend(target.header("{}: {}".format(self.title, self.subtitle), lev=target.ITEM)) 565 | out.extend(subs) 566 | if consts: 567 | out.append("Constants: " + " ".join(consts)) 568 | out.extend(items) 569 | out.append("") 570 | return out 571 | 572 | def get_file_lines(self, controller, target): 573 | """ 574 | Return the markdown for this section. This includes the section 575 | heading and the markdown for the children. 576 | """ 577 | out = [] 578 | if self.subtitle: 579 | out.extend(target.header(str(self), lev=target.SECTION)) 580 | out.extend(target.markdown_block(self.get_markdown_body(controller, target))) 581 | for child in self.children: 582 | out.extend(child.get_file_lines(controller, target)) 583 | return out 584 | 585 | def get_figure_num(self): 586 | hdr = (self.parent.get_figure_num() + ".") if self.parent else "" 587 | return "{}{}".format(hdr, self.figure_num) 588 | 589 | 590 | class SubsectionBlock(GenericBlock): 591 | def __init__(self, title, subtitle, body, origin, parent=None): 592 | super().__init__(title, subtitle, body, origin, parent=parent) 593 | if parent: 594 | self.parent.figure_num += 1 595 | 596 | def get_link(self, target, currfile=None, label="", literalize=False, html=False): 597 | file = self.origin.file 598 | if currfile is None or self.origin.file == currfile: 599 | file = "" 600 | return target.get_link( 601 | label=label if label else str(self), 602 | anchor=target.header_link(str(self)), 603 | file=file, 604 | literalize=literalize, 605 | html=html 606 | ) 607 | 608 | def get_tocfile_lines(self, controller, target, n=1, currfile=""): 609 | """ 610 | Return the markdown table of contents lines for the children in this 611 | subsection. This is returned as a series of bullet points. 612 | `indent` sets the level of indentation for the bullet points 613 | """ 614 | out = [] 615 | item = self.get_link(target, label=self.subtitle, currfile=currfile) 616 | out.extend(target.bullet_list_item(item)) 617 | items = self.get_children_by_title(["Constant","Function","Module","Function&Module"]) 618 | if items: 619 | out.extend( 620 | target.indent_lines( 621 | target.bullet_list( 622 | flatten([ 623 | child.get_tocfile_lines(controller, target, currfile=currfile) 624 | for child in items 625 | ]) 626 | ) 627 | ) 628 | ) 629 | return out 630 | 631 | def get_toc_lines(self, controller, target, n=1, currfile=""): 632 | """ 633 | Return the markdown table of contents lines for the children in this 634 | subsection. This is returned as a series of bullet points. 635 | `indent` sets the level of indentation for the bullet points 636 | """ 637 | lines = [] 638 | for child in self.get_children_by_title(["Constant","Function","Module","Function&Module"]): 639 | lines.extend(child.get_toc_lines(controller, target, currfile=currfile)) 640 | out = [] 641 | if self.subtitle: 642 | item = self.get_link(target, currfile=currfile) 643 | out.extend(target.numbered_list_item(n, item)) 644 | if lines: 645 | out.extend(target.bullet_list_start()) 646 | out.extend(target.indent_lines(lines)) 647 | out.extend(target.bullet_list_end()) 648 | elif lines: 649 | out.extend(target.bullet_list_start()) 650 | out.extend(lines) 651 | out.extend(target.bullet_list_end()) 652 | return out 653 | 654 | def get_cheatsheet_lines(self, controller, target): 655 | consts = [] 656 | for cnst in self.get_children_by_title("Constant"): 657 | consts.append(cnst.get_link(target, currfile="CheatSheet")) 658 | for alias in cnst.aliases: 659 | consts.append(cnst.get_link(target, label=alias, currfile="CheatSheet")) 660 | items = [] 661 | for child in self.get_children_by_title(["Function","Module","Function&Module"]): 662 | items.extend(child.get_cheatsheet_lines(controller, target)) 663 | out = [] 664 | if consts or items: 665 | out.extend(target.header("{}: {}".format(self.title, self.subtitle), lev=target.ITEM)) 666 | if consts: 667 | out.append("Constants: " + " ".join(consts)) 668 | out.extend(items) 669 | out.append("") 670 | return out 671 | 672 | def get_file_lines(self, controller, target): 673 | """ 674 | Return the markdown for this section. This includes the section 675 | heading and the markdown for the children. 676 | """ 677 | out = [] 678 | if self.subtitle: 679 | out.extend(target.header(str(self), lev=target.SUBSECTION)) 680 | out.extend(target.markdown_block(self.get_markdown_body(controller, target))) 681 | for child in self.children: 682 | out.extend(child.get_file_lines(controller, target)) 683 | return out 684 | 685 | def get_figure_num(self): 686 | hdr = (self.parent.get_figure_num() + ".") if self.parent else "" 687 | return "{}{}".format(hdr, self.figure_num) 688 | 689 | 690 | class ItemBlock(LabelBlock): 691 | _paren_pat = re.compile(r'\([^\)]+\)') 692 | 693 | def __init__(self, title, subtitle, body, origin, parent=None): 694 | if self._paren_pat.search(subtitle): 695 | raise DocsGenException(title, "Text between parentheses, while declaring block:") 696 | super().__init__(title, subtitle, body, origin, parent=parent) 697 | self.example_num = 0 698 | self.deprecated = False 699 | self.topics = [] 700 | self.aliases = [] 701 | self.see_also = [] 702 | self.synopsis = "" 703 | self.syntags = {} 704 | if parent: 705 | self.parent.figure_num += 1 706 | 707 | def __str__(self): 708 | return "{}: {}".format( 709 | self.title.replace('&', '/'), 710 | re.sub(r'\([^\)]*\)', r'()', self.subtitle) 711 | ) 712 | 713 | def get_link(self, target, currfile=None, label="", literalize=True, html=False): 714 | file = self.origin.file 715 | if currfile is None or self.origin.file == currfile: 716 | file = "" 717 | return target.get_link( 718 | label=label if label else self.subtitle, 719 | anchor=target.header_link( 720 | "{}: {}".format(self.title, self.subtitle) 721 | ), 722 | file=file, 723 | literalize=literalize, 724 | html=html 725 | ) 726 | 727 | def get_data(self): 728 | d = super().get_data() 729 | if self.deprecated: 730 | d["deprecated"] = True 731 | d["topics"] = self.topics 732 | d["aliases"] = self.aliases 733 | d["synopsis"] = self.synopsis 734 | d["syntags"] = self.syntags 735 | d["see_also"] = self.see_also 736 | d["description"] = [ 737 | line 738 | for item in self.get_children_by_title("Description") 739 | for line in item.body 740 | ] 741 | d["arguments"] = [ 742 | line 743 | for item in self.get_children_by_title("Arguments") 744 | for line in item.body 745 | ] 746 | d["usages"] = [ 747 | { 748 | "subtitle": item.subtitle, 749 | "body": item.body 750 | } 751 | for item in self.get_children_by_title("Usage") 752 | ] 753 | d["examples"] = [ 754 | item.body 755 | for item in self.children if item.title.startswith("Example") 756 | ] 757 | skip_titles = ["Alias", "Aliases", "Arguments", "Description", "See Also", "Synopsis", "SynTags", "Status", "Topics", "Usage"] 758 | d["children"] = list(filter(lambda x: x["name"] not in skip_titles and not x["name"].startswith("Example"), d["children"])) 759 | return d 760 | 761 | def get_synopsis(self, controller, target): 762 | sub = self.parse_links(self.synopsis, controller, target) 763 | out = "{}{}{}".format( 764 | " – " if self.synopsis or self.syntags else "", 765 | target.escape_entities(sub), 766 | target.mouseover_tags(self.syntags, htag="sup", wrap="[{}]"), 767 | ) 768 | return out 769 | 770 | def get_funmod(self): 771 | funmod = self.title 772 | if funmod == "Function": 773 | funmod = "Func" 774 | elif funmod == "Module": 775 | funmod = "Mod" 776 | elif funmod == "Function&Module": 777 | funmod = "Func/Mod" 778 | elif funmod == "Constant": 779 | funmod = "Const" 780 | return funmod 781 | 782 | def get_index_line(self, controller, target, file): 783 | out = "{} {}{}".format( 784 | self.get_link(target, currfile=file), 785 | self.get_funmod(), 786 | self.get_synopsis(controller, target), 787 | ) 788 | return out 789 | 790 | def get_tocfile_lines(self, controller, target, n=1, currfile=""): 791 | out = [ 792 | self.get_index_line(controller, target, currfile) 793 | ] 794 | return out 795 | 796 | def get_toc_lines(self, controller, target, n=1, currfile=""): 797 | out = target.bullet_list_item( 798 | "{}{}".format( 799 | self.get_link(target, currfile=currfile), 800 | self.get_synopsis(controller, target), 801 | ) 802 | ) 803 | return out 804 | 805 | def get_cheatsheet_lines(self, controller, target): 806 | oline = "" 807 | item_name = re.sub(r'[^A-Za-z0-9_$]', r'', self.subtitle) 808 | link = self.get_link(target, currfile="CheatSheet", label=item_name, literalize=False) 809 | parts = [] 810 | part_lens = [] 811 | for usage in self.get_children_by_title("Usage"): 812 | for line in usage.body: 813 | part_lens.append(len(line)) 814 | line = target.escape_entities(line).replace( 815 | target.escape_entities(item_name), 816 | link 817 | ) 818 | parts.append(line) 819 | out = [] 820 | line = "" 821 | line_len = 0 822 | for part, part_len in zip(parts, part_lens): 823 | part = target.code_span(part) 824 | part = target.line_with_break(part)[0] 825 | if line_len + part_len > 80 or part_len > 40: 826 | if line: 827 | line = target.quote(line)[0]; 828 | out.append(line) 829 | line = part 830 | line_len = part_len 831 | else: 832 | if line: 833 | line += "    " 834 | line += part 835 | line_len += part_len 836 | if line: 837 | line = target.quote(line)[0]; 838 | out.extend(target.paragraph([line])) 839 | return out 840 | 841 | def get_file_lines(self, controller, target): 842 | front_blocks = [ 843 | ["Status"], 844 | ["Alias"], 845 | ["Synopsis"], 846 | ["Topics"], 847 | ["See Also"], 848 | ["Usage"] 849 | ] 850 | back_blocks = [ 851 | ["Example"] 852 | ] 853 | children = self.sort_children(front_blocks, back_blocks) 854 | out = [] 855 | out.extend(target.header(str(self), lev=target.ITEM)) 856 | for child in children: 857 | out.extend(child.get_file_lines(controller, target)) 858 | out.extend(target.horizontal_rule()) 859 | return out 860 | 861 | def get_figure_num(self): 862 | hdr = (self.parent.get_figure_num() + ".") if self.parent else "" 863 | return "{}{}".format(hdr, self.figure_num) 864 | 865 | 866 | class LogBlock(GenericBlock): 867 | def __init__(self, title, subtitle, body, origin, parent=None, meta=""): 868 | super().__init__(title, subtitle, body, origin, parent=parent) 869 | self.meta = meta 870 | self.log_output = [] 871 | self.log_title = subtitle if subtitle.strip() else "Log Output" 872 | 873 | fileblock = parent 874 | while fileblock.parent: 875 | fileblock = fileblock.parent 876 | 877 | script_lines = [] 878 | script_lines.extend(fileblock.includes) 879 | script_lines.extend(fileblock.common_code) 880 | for line in self.body: 881 | if line.strip().startswith("--"): 882 | script_lines.append(line.strip()[2:]) 883 | else: 884 | script_lines.append(line) 885 | self.raw_script = script_lines 886 | self.generate_log() 887 | 888 | def generate_log(self): 889 | self.log_request = log_manager.new_request( 890 | self.origin.file, self.origin.line, 891 | self.raw_script, 892 | starting_cb=self._log_proc_start, 893 | completion_cb=self._log_proc_done, 894 | verbose=True 895 | ) 896 | log_manager.process_requests() 897 | 898 | def _log_proc_start(self, req): 899 | print(" Processing log for {}:{}... ".format(self.origin.file, self.origin.line), end='') 900 | sys.stdout.flush() 901 | 902 | def _log_proc_done(self, req): 903 | if req.success: 904 | self.log_output = req.echos 905 | print("SUCCESS") 906 | else: 907 | self.log_output = [] 908 | #print("FAIL") 909 | print("FAIL: " + "\n".join(req.errors + req.warnings)) 910 | sys.stdout.flush() 911 | 912 | def get_file_lines(self, controller, target): 913 | out = [] 914 | #if self.log_output: 915 | if self.log_request.success and self.log_request.echos: 916 | #out.extend(target.block_header("Log Output", "")) 917 | out.extend(target.block_header(self.log_title, "")) 918 | # out.extend(target.markdown_block(["```log"] + self.log_output + ["```"])) 919 | out.extend(target.markdown_block(["```log"] + self.log_request.echos + ["```"])) 920 | else: 921 | print(f"WARNING: No log output for {self.origin.file}:{self.origin.line}") 922 | return out 923 | 924 | class ImageBlock(GenericBlock): 925 | def __init__(self, title, subtitle, body, origin, verbose=False, enabled_features=[], parent=None, meta="", use_apngs=False): 926 | super().__init__(title, subtitle, body, origin, parent=parent) 927 | fileblock = parent 928 | while fileblock.parent: 929 | fileblock = fileblock.parent 930 | 931 | self.meta = meta 932 | self.image_url = None 933 | self.image_url_rel = None 934 | self.image_req = None 935 | 936 | script_lines = [] 937 | script_lines.extend(fileblock.includes) 938 | script_lines.extend(fileblock.common_code) 939 | for line in self.body: 940 | if line.strip().startswith("--"): 941 | script_lines.append(line.strip()[2:]) 942 | else: 943 | script_lines.append(line) 944 | self.raw_script = script_lines 945 | 946 | san_name = re.sub(r'[^A-Za-z0-9_-]', r'', os.path.basename(parent.subtitle.strip().lower().replace(" ","-"))) 947 | if use_apngs: 948 | file_ext = "png" 949 | elif "Spin" in self.meta or "Anim" in self.meta: 950 | file_ext = "gif" 951 | else: 952 | file_ext = "png" 953 | if self.title == "Figure": 954 | parent.figure_num += 1 955 | fignum = self.get_figure_num() 956 | figsan = fignum.replace(".","_") 957 | proposed_name = "figure_{}.{}".format(figsan, file_ext) 958 | self.title = "{} {}".format(self.title, fignum) 959 | else: 960 | parent.example_num += 1 961 | image_num = parent.example_num 962 | img_suffix = "_{}".format(image_num) if image_num > 1 else "" 963 | proposed_name = "{}{}.{}".format(san_name, img_suffix, file_ext) 964 | self.title = "{} {}".format(self.title, image_num) 965 | 966 | file_dir, file_name = os.path.split(fileblock.origin.file.strip()) 967 | file_base = os.path.splitext(file_name)[0] 968 | self.image_url_rel = os.path.join("images", file_base, proposed_name) 969 | self.image_url = os.path.join(file_dir, self.image_url_rel) 970 | self.verbose = verbose 971 | self.enabled_features = enabled_features 972 | 973 | def generate_image(self, target, parser=None): 974 | self.image_req = None 975 | if "NORENDER" in self.meta: 976 | return 977 | show_img = ( 978 | any(x in self.meta for x in ("2D", "3D", "Spin", "Anim")) or 979 | self.title.startswith("Figure") or 980 | self.parent.title in ("File", "LibFile", "Section", "Subsection", "Module", "Function&Module") 981 | ) 982 | if show_img: 983 | outfile = os.path.join(target.docs_dir, self.image_url) 984 | outdir = os.path.dirname(outfile) 985 | os.makedirs(outdir, mode=0o744, exist_ok=True) 986 | default_colorscheme = parser.default_colorscheme if parser else "Cornfield" 987 | self.image_req = image_manager.new_request( 988 | self.origin.file, self.origin.line, 989 | outfile, self.raw_script, self.meta, 990 | starting_cb=self._img_proc_start, 991 | completion_cb=self._img_proc_done, 992 | verbose=self.verbose, 993 | enabled_features=self.enabled_features, 994 | default_colorscheme=default_colorscheme 995 | ) 996 | 997 | def get_data(self): 998 | d = super().get_data() 999 | d["script"] = self.raw_script 1000 | d["imgurl"] = self.image_url 1001 | return d 1002 | 1003 | def _img_proc_start(self, req): 1004 | print(" {}... ".format(os.path.basename(self.image_url)), end='') 1005 | sys.stdout.flush() 1006 | 1007 | def _img_proc_done(self, req): 1008 | if req.success: 1009 | if req.status == "SKIP": 1010 | print() 1011 | else: 1012 | print(req.status) 1013 | sys.stdout.flush() 1014 | return 1015 | pfx = " " 1016 | out = "Failed OpenSCAD script:\n" 1017 | out += pfx + "Image: {}\n".format( os.path.basename(req.image_file) ) 1018 | out += pfx + "cmd-line = {}\n".format(" ".join(req.cmdline)) 1019 | for line in req.stdout: 1020 | out += pfx + line + "\n" 1021 | for line in req.stderr: 1022 | out += pfx + line + "\n" 1023 | out += pfx + "Return code = {}\n".format(req.return_code) 1024 | out += pfx + ("-=" * 32) + "-\n" 1025 | for line in req.script_lines: 1026 | out += pfx + line + "\n" 1027 | out += pfx + ("=-" * 32) + "=" 1028 | print("", file=sys.stderr) 1029 | sys.stderr.flush() 1030 | errorlog.add_entry(req.src_file, req.src_line, out, ErrorLog.FAIL) 1031 | 1032 | def get_file_lines(self, controller, target): 1033 | fileblock = self.parent 1034 | while fileblock.parent: 1035 | fileblock = fileblock.parent 1036 | out = [] 1037 | if "Hide" in self.meta: 1038 | return out 1039 | 1040 | self.generate_image(target, controller) 1041 | 1042 | code = [] 1043 | code.extend([line for line in fileblock.includes]) 1044 | code.extend([line for line in self.body if not line.strip().startswith("--")]) 1045 | 1046 | do_render = "NORENDER" not in self.meta and ( 1047 | self.parent.title in ["Module", "Function&Module"] or 1048 | any(tag in self.meta for tag in ["2D","3D","Spin","Anim"]) 1049 | ) 1050 | 1051 | code_below = False 1052 | width = '' 1053 | height = '' 1054 | if self.image_req: 1055 | code_below = self.image_req.script_under 1056 | width = int(self.image_req.imgsize[0]) 1057 | height = int(self.image_req.imgsize[1]) 1058 | sub = self.parse_links(self.subtitle, controller, target) 1059 | sub = target.escape_entities(sub) 1060 | if "Figure" in self.title: 1061 | out.extend(target.image_block(self.parent.subtitle, self.title, sub, rel_url=self.image_url_rel, code_below=code_below, width=width, height=height)) 1062 | elif not do_render: 1063 | out.extend(target.image_block(self.parent.subtitle, self.title, sub, code=code, code_below=code_below, width=width, height=height)) 1064 | else: 1065 | out.extend(target.image_block(self.parent.subtitle, self.title, sub, code=code, rel_url=self.image_url_rel, code_below=code_below, width=width, height=height)) 1066 | return out 1067 | 1068 | 1069 | class FigureBlock(ImageBlock): 1070 | def __init__(self, title, subtitle, body, origin, parent, verbose=False, enabled_features=[], meta="", use_apngs=False): 1071 | super().__init__(title, subtitle, body, origin, verbose=verbose, enabled_features=enabled_features, parent=parent, meta=meta, use_apngs=use_apngs) 1072 | 1073 | 1074 | class ExampleBlock(ImageBlock): 1075 | def __init__(self, title, subtitle, body, origin, parent, verbose=False, enabled_features=[], meta="", use_apngs=False): 1076 | super().__init__(title, subtitle, body, origin, verbose=verbose, enabled_features=enabled_features, parent=parent, meta=meta, use_apngs=use_apngs) 1077 | 1078 | 1079 | 1080 | 1081 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 1082 | -------------------------------------------------------------------------------- /WRITING_DOCS.md: -------------------------------------------------------------------------------- 1 | Documenting OpenSCAD Code 2 | ------------------------------------------------- 3 | 4 | Documentation comment blocks are all based around a single simple syntax: 5 | 6 | // Block Name(Metadata): TitleText 7 | // Body line 1 8 | // Body line 2 9 | // Body line 3 10 | 11 | - The Block Name is one or two words, both starting with a capital letter. 12 | - The Metadata is in parentheses. It is optional, and can contain fairly arbitrary text, as long as it doesn't include newlines or parentheses. If the Metadata part is not given, the parentheses are optional. 13 | - A colon `:` will always follow after the Block Name and optional Metadata. 14 | - The TitleText will be preceded by a space ` `, and can contain arbitrary text, as long as it contains no newlines. The TitleText part is also optional for some header blocks. 15 | - The body will contain zero or more lines of text indented by three spaces after the comment markers. Each line can contain arbitrary text. 16 | 17 | So, for example, a Figure block to show a 640x480 animated GIF of a spinning shape may look like: 18 | 19 | // Figure(Spin,Size=640x480,VPD=444): A Cube and Cylinder. 20 | // cube(80, center=true); 21 | // cylinder(h=100,d=60,center=true); 22 | 23 | Various block types don't need all of those parts, so they may look simpler: 24 | 25 | // Topics: Mask, Cylindrical, Attachable 26 | 27 | Or: 28 | 29 | // Description: 30 | // This is a description. 31 | // It can be multiple lines in length. 32 | 33 | Or: 34 | 35 | // Usage: Typical Usage 36 | // x = foo(a, b, c); 37 | // x = foo([a, b, c, ...]); 38 | 39 | Comments blocks that don't start with a known block header are ignored and not added to output documentation. This lets you have normal comments in your code that are not used for documentation. If you must start a comment block with one of the known headers, then adding a single extra `/` or space after the comment marker, will make it be treated as a regular comment: 40 | 41 | /// File: Foobar.scad 42 | 43 | 44 | Block Headers 45 | ======================= 46 | 47 | File/LibFile Blocks 48 | ------------------- 49 | 50 | All files must have either a `// File:` block or a `// LibFile:` block at the start. This is the place to put in the canonical filename, and a description of what the file is for. These blocks can be used interchangably, but you can only have one per file. `// File:` or `// LibFile:` blocks can be followed by a multiple line body that are added as markdown text after the header: 51 | 52 | // LibFile: foo.scad 53 | // You can have several lines of markdown formatted text here. 54 | // You just need to make sure that each line is indented, with 55 | // at least three spaces after the comment marker. You can 56 | // denote a paragraph break with a comment line with three 57 | // trailing spaces, or just a period. 58 | // . 59 | // You can have links in this text to functions, modules, or 60 | // constants in other files by putting the name in double- 61 | // braces like {{cyl()}} or {{lerp()}} or {{DOWN}}. If you want to 62 | // link to another file, or section in another file you can use 63 | // a manual markdown link like [Section: Cuboids](shapes.scad#section-cuboids). 64 | // The end of the block is denoted by a line without a comment. 65 | 66 | Which outputs Markdown code that renders like: 67 | 68 | > ## LibFile: foo.scad 69 | > You can have several lines of markdown formatted text here. 70 | > You just need to make sure that each line is indented, with 71 | > at least three spaces after the comment marker. You can 72 | > denote a paragraph break with a comment line with three 73 | > trailing spaces, or just a period. 74 | > 75 | > You can have links in this text to functions, modules, or 76 | > constants in other files by putting the name in double- 77 | > braces like [cyl()](shapes.scad#functionmodule-cyl) or [lerp()](math.scad#function-lerp) or [DOWN](constants.scad-down). If you want to 78 | > link to another file, or section in another file you can use 79 | > a manual markdown link like [Section: Cuboids](shapes.scad#section-cuboids). 80 | > The end of the block is denoted by a line without a comment. 81 | 82 | You can use `// File:` instead of `// LibFile:`, if it seems more apropriate for your particular context: 83 | 84 | // File: Foobar.scad 85 | // This file contains a collection of metasyntactical nonsense. 86 | 87 | Which outputs Markdown code that renders like: 88 | 89 | > # File: Foobar.scad 90 | > This file contains a collection of metasyntactical nonsense. 91 | 92 | 93 | FileGroup Block 94 | --------------- 95 | 96 | You can specify what group of files this .scad file is a part of with the `// FileGroup:` block: 97 | 98 | // FileGroup: Advanced Modeling 99 | 100 | This affects the ordering of files in Table of Contents and CheatSheet files. This doesn't generate any output text otherwise. 101 | 102 | 103 | FileSummary Block 104 | ----------------- 105 | 106 | You can give a short summary of the contents of this .scad file with the `// FileSummary:` block: 107 | 108 | // FileSummary: Various modules to generate Foobar objects. 109 | 110 | This summary is used when summarizing this .scad file in the Table of Contents file. This will result in a line in the Table of Contents that renders like: 111 | 112 | > - [Foobar.scad](Foobar.scad): Various modules to generate Foobar objects. 113 | 114 | 115 | FileFootnotes Block 116 | ------------------- 117 | 118 | You can specify footnotes that are appended to this .scad file's name wherever the list of files is shown, such as in the Table of Contents. You can do this with the `// FileFootnotes:` block. The syntax looks like: 119 | 120 | // FileFootnotes: 1=First Footnote; 2=Second Footnote 121 | 122 | Multiple footnotes are separated by semicolons (`;`). Within each footnote, you specify the footnote symbol and the footnote text separated by an equals sign (`=`). The footnote symbol may be more than one character, like this: 123 | 124 | // FileFootnotes: STD=Included in std.scad 125 | 126 | This will result in footnote markers that render like: 127 | 128 | > - Foobar.scad[STD](#footnote-std "Included in std.scad") 129 | 130 | 131 | Includes Block 132 | -------------- 133 | 134 | To declare what code the user needs to add to their code to include or use this library file, you can use the `// Includes:` block. You should put this right after the `// File:` or `// LibFile:` block. This code block will also be prepended to all Example and Figure code blocks before they are evaluated: 135 | 136 | // Includes: 137 | // include 138 | // include 139 | 140 | Which outputs Markdown code that renders like: 141 | 142 | > **Includes:** 143 | > 144 | > To use, add the following lines to the beginning of your file: 145 | > 146 | > ```openscad 147 | > include 148 | > include 149 | > ``` 150 | 151 | 152 | CommonCode Block 153 | ---------------- 154 | 155 | If you have a block of code you plan to use throughout the file's Figure or Example blocks, and you don't actually want it displayed, you can use a `// CommonCode:` block like thus: 156 | 157 | // CommonCode: 158 | // module text3d(text, h=0.01, size=3) { 159 | // linear_extrude(height=h, convexity=10) { 160 | // text(text=text, size=size, valign="center", halign="center"); 161 | // } 162 | // } 163 | 164 | This doesn't have immediately visible markdown output, but you *can* use that code in later examples: 165 | 166 | // Example: 167 | // text3d("Foobar"); 168 | 169 | 170 | Section Block 171 | ------------- 172 | 173 | Section blocks take a title, and an optional body that will be shown as the description of the Section. If a body line if just a `.` (dot, period), then that line is treated as a blank line in the output: 174 | 175 | // Section: Foobar 176 | // You can have several lines of markdown formatted text here. 177 | // You just need to make sure that each line is indented, with 178 | // at least three spaces after the comment marker. You can 179 | // denote a paragraph break with a comment line with three 180 | // trailing spaces, or just a period. 181 | // . 182 | // You can have links in this text to functions, modules, or 183 | // constants in other files by putting the name in double- 184 | // braces like {{cyl()}} or {{lerp()}} or {{DOWN}}. If you want to 185 | // link to another file, or section in another file you can use 186 | // a manual markdown link like [Section: Cuboids](shapes.scad#section-cuboids). 187 | // . 188 | // The end of the block is denoted by a line without a comment. 189 | // or a line that is unindented after the comment. 190 | 191 | Which outputs Markdown code that renders like: 192 | 193 | > ## Section: Foobar 194 | > You can have several lines of markdown formatted text here. 195 | > You just need to make sure that each line is indented, with 196 | > at least three spaces after the comment marker. You can 197 | > denote a paragraph break with a comment line with three 198 | > trailing spaces, or just a period. 199 | > 200 | > You can have links in this text to functions, modules, or 201 | > constants in other files by putting the name in double- 202 | > braces like [cyl()](shapes.scad#functionmodule-cyl) or [lerp()](math.scad#function-lerp) or [DOWN](constants.scad-down). If you want to 203 | > link to another file, or section in another file you can use 204 | > a manual markdown link like [Section: Cuboids](shapes.scad#section-cuboids). 205 | > 206 | > The end of the block is denoted by a line without a comment. 207 | > or a line that is unindented after the comment. 208 | 209 | Sections can also include: 210 | 211 | - Figures: images generated from code that is not shown in a code block. 212 | - Definitions: Glossary term definitions. 213 | 214 | 215 | Subsection Block 216 | ---------------- 217 | 218 | Subsection blocks take a title, and an optional body that will be shown as the description of the Subsection. A Subsection must be within a declared Section. If a body line is just a `.` (dot, period), then that line is treated as a blank line in the output: 219 | 220 | // Subsection: Foobar 221 | // You can have several lines of markdown formatted text here. 222 | // You just need to make sure that each line is indented, with 223 | // at least three spaces after the comment marker. You can 224 | // denote a paragraph break with a comment line with three 225 | // trailing spaces, or just a period. 226 | // . 227 | // You can have links in this text to functions, modules, or 228 | // constants in other files by putting the name in double- 229 | // braces like {{cyl()}} or {{lerp()}} or {{DOWN}}. If you want to 230 | // link to another file, or section in another file you can use 231 | // a manual markdown link like [Subsection: Foo](shapes.scad#subsection-foo). 232 | // . 233 | // The end of the block is denoted by a line without a comment. 234 | // or a line that is unindented after the comment. 235 | 236 | Which outputs Markdown code that renders like: 237 | 238 | > ## Subsection: Foobar 239 | > You can have several lines of markdown formatted text here. 240 | > You just need to make sure that each line is indented, with 241 | > at least three spaces after the comment marker. You can 242 | > denote a paragraph break with a comment line with three 243 | > trailing spaces, or just a period. 244 | > 245 | > You can have links in this text to functions, modules, or 246 | > constants in other files by putting the name in double- 247 | > braces like [cyl()](shapes.scad#functionmodule-cyl) or [lerp()](math.scad#function-lerp) or [DOWN](constants.scad-down). If you want to 248 | > link to another file, or section in another file you can use 249 | > a manual markdown link like [Subsection: Foo](shapes.scad#subsection-foo). 250 | > 251 | > The end of the block is denoted by a line without a comment. 252 | > or a line that is unindented after the comment. 253 | 254 | Subsections can also include: 255 | 256 | - Figures: images generated from code that is not shown in a code block. 257 | - Definitions: Glossary term definitions. 258 | 259 | 260 | Item Blocks 261 | ----------- 262 | 263 | Item blocks headers come in four varieties: `Constant`, `Function`, `Module`, and `Function&Module`. 264 | 265 | The `Constant` header is used to document a code constant. It should have a Description sub-block, and Example sub-blocks are recommended: 266 | 267 | // Constant: PHI 268 | // Description: The golden ratio phi. 269 | PHI = (1+sqrt(5))/2; 270 | 271 | Which outputs Markdown code that renders like: 272 | 273 | > ### Constant: PHI 274 | > **Description:** 275 | > The golden ration phi. 276 | 277 | 278 | The `Module` header is used to document a module. It should have a Description sub-block. It is recommended to also have Usage, Arguments, and Example/Examples sub-blocks. The Usage sub-block body lines are also used when constructing the Cheat Sheet index file: 279 | 280 | // Module: cross() 281 | // Usage: 282 | // cross(size); 283 | // Description: 284 | // Creates a 2D cross/plus shape. 285 | // Arguments: 286 | // size = The scalar size of the cross. 287 | // Example(2D): 288 | // cross(size=100); 289 | module cross(size=1) { 290 | square([size, size/3], center=true); 291 | square([size/3, size], center=true); 292 | } 293 | 294 | Which outputs Markdown code that renders like: 295 | 296 | > ### Module: cross() 297 | > **Usage:** 298 | > - cross(size); 299 | > 300 | > **Description:** 301 | > Creates a 2D cross/plus shape. 302 | > 303 | > **Arguments:** 304 | > Positional Arg | What it does 305 | > -------------------- | ------------------- 306 | > size | The scalar size of the cross. 307 | > 308 | > **Example:** 309 | > ```openscad 310 | > cross(size=100); 311 | > ``` 312 | > GENERATED IMAGE GOES HERE 313 | 314 | 315 | The `Function` header is used to document a function. It should have a Description sub-block. It is recommended to also have Usage, Arguments, and Example/Examples sub-blocks. By default, Examples will not generate images for function blocks. Usage sub-block body lines are also used when constructing the Cheat Sheet index file: 316 | 317 | // Function: vector_angle() 318 | // Usage: 319 | // ang = vector_angle(v1, v2); 320 | // Description: 321 | // Calculates the angle between two vectors in degrees. 322 | // Arguments: 323 | // v1 = The first vector. 324 | // v2 = The second vector. 325 | // Example: 326 | // v1 = [1,1,0]; 327 | // v2 = [1,0,0]; 328 | // angle = vector_angle(v1, v2); 329 | // // Returns: 45 330 | function vector_angle(v1,v2) = 331 | acos(max(-1,min(1,(vecs[0]*vecs[1])/(norm0*norm1)))); 332 | 333 | Which outputs Markdown code that renders like: 334 | 335 | > ### Function: vector_angle() 336 | > **Usage:** 337 | > - ang = vector_angle(v1, v2); 338 | > 339 | > **Description:** 340 | > Calculates the angle between two vectors in degrees. 341 | > 342 | > **Arguments:** 343 | > Positional Arg | What it does 344 | > -------------------- | ------------------- 345 | > `v1` | The first vector. 346 | > `v2` | The second vector. 347 | > 348 | > **Example:** 349 | > ```openscad 350 | > v1 = [1,1,0]; 351 | > v2 = [1,0,0]; 352 | > angle = vector_angle(v1, v2); 353 | > // Returns: 45 354 | > ``` 355 | 356 | The `Function&Module` header is used to document a function which has a related module of the same name. It should have a Description sub-block. It is recommended to also have Usage, Arguments, and Example/Examples sub-blocks. You should have Usage blocks for both calling as a function, and calling as a module. Usage sub-block body lines are also used in constructing the Cheat Sheet index file: 357 | 358 | // Function&Module: oval() 359 | // Synopsis: Creates an Ovel shape. 360 | // Topics: 2D Shapes, Geometry 361 | // Usage: As a Module 362 | // oval(rx,ry); 363 | // Usage: As a Function 364 | // path = oval(rx,ry); 365 | // Description: 366 | // When called as a function, returns the perimeter path of the oval. 367 | // When called as a module, creates a 2D oval shape. 368 | // Arguments: 369 | // rx = X axis radius. 370 | // ry = Y axis radius. 371 | // Example(2D): Called as a Function 372 | // path = oval(100,60); 373 | // polygon(path); 374 | // Example(2D): Called as a Module 375 | // oval(80,60); 376 | module oval(rx,ry) { 377 | polygon(oval(rx,ry)); 378 | } 379 | function oval(rx,ry) = 380 | [for (a=[360:-360/$fn:0.0001]) [rx*cos(a),ry*sin(a)]; 381 | 382 | Which outputs Markdown code that renders like: 383 | 384 | > ### Function&Module: oval() 385 | > **Synopsis:** Creates an oval shape. 386 | > **Topics:** 2D Shapes, Geometry 387 | > 388 | > **Usage:** As a Module 389 | > 390 | > - oval(rx,ry); 391 | > 392 | > **Usage:** As a Function 393 | > 394 | > - path = oval(rx,ry); 395 | > 396 | > **Description:** 397 | > When called as a function, returns the perimeter path of the oval. 398 | > When called as a module, creates a 2D oval shape. 399 | > 400 | > **Arguments:** 401 | > Positional Arg | What it does 402 | > -------------------- | ------------------- 403 | > rx | X axis radius. 404 | > ry | Y axis radius. 405 | > 406 | > **Example:** Called as a Function 407 | > 408 | > ```openscad 409 | > path = oval(100,60); 410 | > polygon(path); 411 | > ``` 412 | > GENERATED IMAGE SHOWN HERE 413 | > 414 | > **Example:** Called as a Module 415 | > 416 | > ```openscad 417 | > oval(80,60); 418 | > ``` 419 | > GENERATED IMAGE SHOWN HERE 420 | 421 | These Type blocks can have a number of sub-blocks. Most sub-blocks are optional, The available standard sub-blocks are: 422 | 423 | - `// Aliases: alternatename(), anothername()` 424 | - `// Status: DEPRECATED` 425 | - `// Synopsis: A short description.` 426 | - `// SynTags: VNF, Geom` 427 | - `// Topics: Comma, Delimited, Topic, List` 428 | - `// See Also: otherfunc(), othermod(), OTHERCONST` 429 | - `// Usage:` 430 | - `// Description:` 431 | - `// Figure:` or `// Figures` 432 | - `// Continues:` 433 | - `// Arguments:` 434 | - `// Example:` or `// Examples:` 435 | - `// Definitions:` 436 | 437 | 438 | Aliases Block 439 | ------------- 440 | 441 | The Aliases block is used to give alternate names for a function, module, or 442 | constant. This is reflected in the indexes generated. It looks like: 443 | 444 | // Aliases: secondname(), thirdname() 445 | 446 | Which outputs Markdown code that renders like: 447 | 448 | > **Aliases:** secondname(), thirdname() 449 | 450 | 451 | Status Block 452 | ------------ 453 | 454 | The Status block is used to mark a function, module, or constant as deprecated: 455 | 456 | // Status: DEPRECATED, use foo() instead 457 | 458 | Which outputs Markdown code that renders like: 459 | 460 | > **Status:** DEPRECATED, use foo() instead 461 | 462 | 463 | Synopsis Block 464 | -------------- 465 | 466 | The Synopsis block gives a short one-line description of the current function or module. This is shown in various indices: 467 | 468 | // Synopsis: A short one-line description. 469 | 470 | Which outputs Markdown code that renders like: 471 | 472 | > **Synopsis:** A short one-line description. 473 | 474 | 475 | SynTags Block 476 | ------------- 477 | 478 | The SynTags block can be used with the Synopsis block, and the DefineSynTags configuration file block, 479 | to allow you to add hover-text tags to the end of Synopsis lines. This is shown in various indices: 480 | 481 | In the .openscad_docsgen_rc config file: 482 | 483 | DefineSynTags: 484 | VNF = Can return an VNF when called as a function. 485 | Geom = Can return geometry when called as a module. 486 | Path = Can return a Path when called as a function. 487 | 488 | In scadfile documentation: 489 | 490 | // Synopsis: Creates a weird shape. 491 | // SynTags: VNF, Geom 492 | 493 | Which outputs Markdown code that renders like: 494 | 495 | > **Synopsis:** Creates a weird shape. \[VNF\] \[Geom\] 496 | 497 | 498 | Topics Block 499 | ------------ 500 | 501 | The Topics block can associate various topics with the current function or module. This can be used to make an index of Topics: 502 | 503 | // Topics: 2D Shapes, Geometry, Masks 504 | 505 | Which outputs Markdown code that renders like: 506 | 507 | > **Topics:** 2D Shapes, Geometry, Masks 508 | 509 | 510 | See Also Block 511 | -------------- 512 | 513 | The See Also block is used to give links to related functions, modules, or 514 | constants. It looks like: 515 | 516 | // See Also: relatedfunc(), similarmodule() 517 | 518 | Which outputs Markdown code that renders like: 519 | 520 | > **See Also:** [relatedfunc()](otherfile.scad#relatedfunc), [similarmodule()](otherfile.scad#similarmodule) 521 | 522 | 523 | Usage Block 524 | ----------- 525 | 526 | The Usage block describes the various ways that the current function or module can be called, with the names of the arguments. By convention, the first few arguments that can be called positionally just have their name shown. The remaining arguments that should be passed by name, will have the name followed by an `=` (equal sign). Arguments that are optional in the given Usage context are shown in `[` and `]` angle brackets. Usage sub-block body lines are also used when constructing the Cheat Sheet index file: 527 | 528 | // Usage: As a Module 529 | // oval(rx, ry, ); 530 | // Usage: As a Function 531 | // path = oval(rx, ry, ); 532 | 533 | Which outputs Markdown code that renders like: 534 | 535 | > **Usage:** As a Module 536 | > - oval(rx, ry, ); 537 | > 538 | > **Usage:** As a Function 539 | > 540 | > - path = oval(rx, ry, ); 541 | 542 | 543 | Description Block 544 | ----------------- 545 | The Description block just describes the currect function, module, or constant: 546 | 547 | // Descripton: This is the description for this function or module. 548 | // It can be multiple lines long. Markdown syntax code will be used 549 | // verbatim in the output markdown file, with the exception of `_`, 550 | // which will traslate to `\_`, so that underscores in function/module 551 | // names don't get butchered. A line with just a period (`.`) will be 552 | // treated as a blank line. 553 | // . 554 | // You can have links in this text to functions, modules, or 555 | // constants in other files by putting the name in double- 556 | // braces like {{cyl()}} or {{lerp()}} or {{DOWN}}. If you want to 557 | // link to another file, or section in another file you can use 558 | // a manual markdown link like [Section: Cuboids](shapes.scad#section-cuboids). 559 | 560 | Which outputs Markdown code that renders like: 561 | 562 | > **Description:** 563 | > It can be multiple lines long. Markdown syntax code will be used 564 | > verbatim in the output markdown file, with the exception of `_`, 565 | > which will traslate to `\_`, so that underscores in function/module 566 | > names don't get butchered. A line with just a period (`.`) will be 567 | > treated as a blank line. 568 | > 569 | > You can have links in this text to functions, modules, or 570 | > constants in other files by putting the name in double- 571 | > braces like [cyl()](shapes.scad#functionmodule-cyl) or [lerp()](math.scad#function-lerp) or [DOWN](constants.scad-down). If you want to 572 | > link to another file, or section in another file you can use 573 | > a manual markdown link like [Section: Cuboids](shapes.scad#section-cuboids). 574 | 575 | 576 | Continues Block 577 | --------------- 578 | The Continues block can be used to continue the body text of a previous block that has been interrupted by a Figure: 579 | 580 | // Descripton: This is the description for this function or module. It can be 581 | // many lines long. If you need to show an image in the middle of this text, 582 | // you can use a Figure, like this: 583 | // Figure(2D): A circle with a square cutout. 584 | // difference() { 585 | // circle(d=100); 586 | // square(100/sqrt(2), center=true); 587 | // } 588 | // Continues: You can continue the description text here. It can also be 589 | // multiple lines long. This continuation will not print a header. 590 | 591 | Which outputs Markdown code that renders like: 592 | 593 | > **Descripton:** 594 | > This is the description for this function or module. It can be 595 | > many lines long. If you need to show an image in the middle of this text, 596 | > you can use a Figure, like this: 597 | > 598 | > **Figure 1:** A circle with a square cutout. 599 | > GENERATED IMAGE SHOWN HERE 600 | > 601 | > You can continue the description text here. It can also be 602 | > multiple lines long. This continuation will not print a header. 603 | > 604 | 605 | 606 | Arguments Block 607 | --------------- 608 | The Arguments block creates a table that describes the positional arguments for a function or module, and optionally a second table that describes named arguments: 609 | 610 | // Arguments: 611 | // v1 = This supplies the first vector. 612 | // v2 = This supplies the second vector. 613 | // --- 614 | // fast = Use fast, but less comprehensive calculation method. 615 | // bar = Takes an optional `bar` struct. See {{bar()}}. 616 | // dflt = Default value. 617 | 618 | Which outputs Markdown code that renders like: 619 | 620 | > **Arguments:** 621 | > Positional Arg | What it Does 622 | > -------------- | --------------------------------- 623 | > `v1` | This supplies the first vector. 624 | > `v2` | The supplies the second vector. 625 | > 626 | > Named Arg | What it Does 627 | > -------------- | --------------------------------- 628 | > `fast` | If true, use fast, but less accurate calculation method. 629 | > `bar` | Takes an optional `bar` struct. See [bar()](foobar.scad#function-bar). 630 | > `dflt` | Default value. 631 | 632 | 633 | Figure Block 634 | -------------- 635 | 636 | A Figure block generates and shows an image from a script in the multi-line body, by running it in OpenSCAD. A Figures block (plural) does the same, but treats each line of the body as a separate Figure block: 637 | 638 | // Figure: Figure description 639 | // cylinder(h=100, d1=75, d2=50); 640 | // up(100) cylinder(h=100, d1=50, d2=75); 641 | // Figure(Spin,VPD=444): Animated figure that spins to show all faces. 642 | // cube([10,100,50], center=true); 643 | // cube([100,10,30], center=true); 644 | // Figures: 645 | // cube(100); 646 | // cylinder(h=100,d=50); 647 | // sphere(d=100); 648 | 649 | Which outputs Markdown code that renders like: 650 | 651 | > **Figure 1:** Figure description 652 | > GENERATED IMAGE SHOWN HERE 653 | > 654 | > **Figure 2:** Animated figure that spins to show all faces. 655 | > GENERATED IMAGE SHOWN HERE 656 | > 657 | > **Figure 3:** 658 | > GENERATED IMAGE OF CUBE SHOWN HERE 659 | > 660 | > **Figure 4:** 661 | > GENERATED IMAGE OF CYLINDER SHOWN HERE 662 | > 663 | > **Figure 5:** 664 | > GENERATED IMAGE OF SPHERE SHOWN HERE 665 | 666 | The metadata of the Figure block can contain various directives to alter how 667 | the image will be generated. These can be comma separated to give multiple 668 | metadata directives: 669 | 670 | - `NORENDER`: Don't generate an image for this example, but show the example text. 671 | - `Hide`: Generate, but don't show script or image. This can be used to generate images to be manually displayed in markdown text blocks. 672 | - `2D`: Orient camera in a top-down view for showing 2D objects. 673 | - `3D`: Orient camera in an oblique view for showing 3D objects. 674 | - `VPT=[10,20,30]` Force the viewpoint translation `$vpt` to `[10,20,30]`. 675 | - `VPR=[55,0,600]` Force the viewpoint rotation `$vpr` to `[55,0,60]`. 676 | - `VPD=440`: Force viewpoint distance `$vpd` to 440. 677 | - `VPF=22.5`: Force field of view angle `$vpf` to 22.5. 678 | - `Spin`: Animate camera orbit around the `[0,1,1]` axis to display all sides of an object. 679 | - `FlatSpin`: Animate camera orbit around the Z axis, above the XY plane. 680 | - `XSpin`: Animate camera orbit around the X axis, to the right of the YZ plane. 681 | - `YSpin`: Animate camera orbit around the Y axis, to the front of the XZ plane. 682 | - `Anim`: Make an animation where `$t` varies from `0.0` to almost `1.0`. 683 | - `FrameMS=250`: Sets the number of milliseconds per frame for spins and animation. 684 | - `FPS=8`: Sets the number of frames per second for spins and animation. 685 | - `Frames=36`: Number of animation frames to make. 686 | - `Small`: Make the image small sized. 687 | - `Med`: Make the image medium sized. 688 | - `Big`: Make the image big sized. 689 | - `Huge`: Make the image huge sized. 690 | - `Size=880x640`: Make the image 880 by 640 pixels in size. 691 | - `ThrownTogether`: Render in Thrown Together view mode instead of Preview mode. 692 | - `Render`: Force full rendering from OpenSCAD, instead of the normal Preview mode. 693 | - `Edges`: Highlight face edges. 694 | - `NoAxes`: Hides the axes and scales. 695 | - `NoScales`: Hides the scale numbers along the axes. 696 | - `ScriptUnder`: Display script text under image, rather than beside it. 697 | 698 | 699 | Example Block 700 | ------------- 701 | 702 | An Example block shows a script, and possibly shows an image generated from it. 703 | The script is in the multi-line body. The `Examples` (plural) block does 704 | the same, but it treats eash body line as a separate Example bloc to show. 705 | Any images, if generated, will be created by running it in OpenSCAD: 706 | 707 | // Example: Example description 708 | // cylinder(h=100, d1=75, d2=50); 709 | // up(100) cylinder(h=100, d1=50, d2=75); 710 | // Example(Spin,VPD=444): Animated shape that spins to show all faces. 711 | // cube([10,100,50], center=true); 712 | // cube([100,10,30], center=true); 713 | // Examples: 714 | // cube(100); 715 | // cylinder(h=100,d=50); 716 | // sphere(d=100); 717 | 718 | Which outputs Markdown code that renders like: 719 | 720 | > **Example 1:** Example description 721 | > ```openscad 722 | > cylinder(h=100, d1=75, d2=50); 723 | > up(100) cylinder(h=100, d1=50, d2=75); 724 | > ``` 725 | > GENERATED IMAGE SHOWN HERE 726 | > 727 | > **Example 2:** Animated shape that spins to show all faces. 728 | > ```openscad 729 | > cube([10,100,50], center=true); 730 | > cube([100,10,30], center=true); 731 | > ``` 732 | > GENERATED IMAGE SHOWN HERE 733 | > 734 | > **Example 3:** 735 | > ```openscad 736 | > cube(100); 737 | > ``` 738 | > GENERATED IMAGE OF CUBE SHOWN HERE 739 | > 740 | > **Example 4:** 741 | > ```openscad 742 | > cylinder(h=100,d=50); 743 | > ``` 744 | > GENERATED IMAGE OF CYLINDER SHOWN HERE 745 | > 746 | > **Example 5:** 747 | > ```openscad 748 | > sphere(d=100); 749 | > ``` 750 | > GENERATED IMAGE OF SPHERE SHOWN HERE 751 | 752 | The metadata of the Example block can contain various directives to alter how 753 | the image will be generated. These can be comma separated to give multiple 754 | metadata directives: 755 | 756 | - `NORENDER`: Don't generate an image for this example, but show the example text. 757 | - `Hide`: Generate, but don't show script or image. This can be used to generate images to be manually displayed in markdown text blocks. 758 | - `2D`: Orient camera in a top-down view for showing 2D objects. 759 | - `3D`: Orient camera in an oblique view for showing 3D objects. Often used to force an Example sub-block to generate an image in Function and Constant blocks. 760 | - `VPT=[10,20,30]` Force the viewpoint translation `$vpt` to `[10,20,30]`. 761 | - `VPR=[55,0,600]` Force the viewpoint rotation `$vpr` to `[55,0,60]`. 762 | - `VPD=440`: Force viewpoint distance `$vpd` to 440. 763 | - `VPF=22.5`: Force field of view angle `$vpf` to 22.5. 764 | - `Spin`: Animate camera orbit around the `[0,1,1]` axis to display all sides of an object. 765 | - `FlatSpin`: Animate camera orbit around the Z axis, above the XY plane. 766 | - `XSpin`: Animate camera orbit around the X axis, to the right of the YZ plane. 767 | - `YSpin`: Animate camera orbit around the Y axis, to the front of the XZ plane. 768 | - `Anim`: Make an animation where `$t` varies from `0.0` to almost `1.0`. 769 | - `FrameMS=250`: Sets the number of milliseconds per frame for spins and animation. 770 | - `FPS=8`: Sets the number of frames per second for spins and animation. 771 | - `Frames=36`: Number of animation frames to make. 772 | - `Small`: Make the image small sized. 773 | - `Med`: Make the image medium sized. 774 | - `Big`: Make the image big sized. 775 | - `Huge`: Make the image huge sized. 776 | - `Size=880x640`: Make the image 880 by 640 pixels in size. 777 | - `Render`: Force full rendering from OpenSCAD, instead of the normal preview. 778 | - `Edges`: Highlight face edges. 779 | - `NoAxes`: Hides the axes and scales. 780 | - `NoScales`: Hides the scale numbers along the axes. 781 | - `ScriptUnder`: Display script text under image, rather than beside it. 782 | - `ColorScheme`: Generate the image using a specific color scheme 783 | - Usage: `ColorScheme=` (e.g. `ColorScheme=BeforeDawn`) 784 | - Default color scheme: `Cornfield` 785 | - Predefined color schemes: `Cornfield`, `Metallic`, `Sunset`, `Starnight`, `BeforeDawn`, `Nature`, `DeepOcean`, `Solarized`, `Tomorrow`, `Tomorrow Night`, `Monotone` 786 | - Color schemes defined as a [Read-only Resource](https://github.com/openscad/openscad/wiki/Path-locations#read-only-resources) or [User Resource](https://github.com/openscad/openscad/wiki/Path-locations#user-resources) are also supported. 787 | 788 | Modules will default to generating and displaying the image as if the `3D` 789 | directive is given. Functions and constants will default to not generating 790 | an image unless `3D`, `Spin`, `FlatSpin` or `Anim` is explicitly given. 791 | 792 | If any lines of the Example script begin with `--`, then they are not shown in 793 | the example script output to the documentation, but they *are* included in the 794 | script used to generate the example image, without the `--`, of course: 795 | 796 | // Example: Multi-line example. 797 | // --$fn = 72; // Lines starting with -- aren't shown in docs example text. 798 | // lst = [ 799 | // "multi-line examples", 800 | // "are shown in one block", 801 | // "with a single image.", 802 | // ]; 803 | // foo(lst, 23, "blah"); 804 | 805 | 806 | Definitions Block 807 | ----------------- 808 | 809 | A Definitions block is used to define one or more terms that will be included in the Glossary.md file. The definitions are also shown where they are defined in the docs. Terms are defined one per line of the body, and have the term and definition separated by an `=` sign. A term can have aliases, separated by `|` bar characters. For example: 810 | 811 | // Definitions: 812 | // Path|Paths = A list of 2D point coordinates, defining a polyline. 813 | // Polygon|Polygons = A path where the first and last points are connected. 814 | // Convex Polygon|Convex Polygons = A polygon such that no extended side itersects any other side or vertex. 815 | 816 | Which outputs Markdown code that renders like: 817 | 818 | > **Definitions:** 819 | >
820 | >
Path
821 | >
A list of 2D coordinates, defining a polyline.
822 | >
Polygon
823 | >
A path where the first and last points are connected.
824 | >
Convex Polygon
825 | >
A polygon such that no extended side itersects any other side or vertex.
826 | >
827 | > 828 | 829 | Creating Custom Block Headers 830 | ============================= 831 | 832 | If you have need of a non-standard documentation block in your docs, you can declare the new block type using `DefineHeader:`. This has the syntax: 833 | 834 | // DefineHeader(TYPE): NEWBLOCKNAME 835 | 836 | or: 837 | 838 | // DefineHeader(TYPE;OPTIONS): NEWBLOCKNAME 839 | 840 | Where NEWBLOCKNAME is the name of the new block header, OPTIONS is an optional list of zero or more semicolon-separated header options, and TYPE defines the behavior of the new block. TYPE can be one of: 841 | 842 | - `Generic`: Show both the TitleText and body. 843 | - `Text`: Show the TitleText as the first line of the body. 844 | - `Headerless`: Show the TitleText as the first line of the body, with no header line. 845 | - `Label`: Show only the TitleText and no body. 846 | - `NumList`: Shows TitleText, and the body lines in a numbered list. 847 | - `BulletList`: Shows TitleText, and the body lines in a bullet list. 848 | - `Table`: Shows TitleText, and body lines in a definition table. 849 | - `Figure`: Shows TitleText, and an image rendered from the script in the Body. 850 | - `Example`: Like Figure, but also shows the body as an example script. 851 | 852 | The OPTIONS are zero or more semicolon separated options for defining the header options. Some of them only require the option name, like `Foo`, and some have an option name and a value separated by an equals sign, like `Foo=Bar`. There is currently only one option common to all header types: 853 | 854 | - `ItemOnly`: Specify that the new header is only allowed as part of the documentation block for a Constant, Function, or Module. 855 | 856 | Generic Block Type 857 | ------------------ 858 | 859 | The Generic block header type takes both title and body lines and generates a markdown block that has the block header, title, and a following body: 860 | 861 | // DefineHeader(Generic): Result 862 | // Result: For Typical Cases 863 | // Does typical things. 864 | // Or something like that. 865 | // Refer to {{stuff()}} for more info. 866 | // Result: For Atypical Cases 867 | // Performs an atypical thing. 868 | 869 | Which outputs Markdown code that renders like: 870 | 871 | > **Result:** For Typical Cases 872 | > 873 | > Does typical things. 874 | > Or something like that. 875 | > Refer to [stuff()](foobar.scad#function-stuff) for more info. 876 | > 877 | > **Result:** For Atypical Cases 878 | > 879 | > Performs an atypical thing. 880 | > 881 | 882 | 883 | Text Block Type 884 | --------------- 885 | 886 | The Text block header type is similar to the Generic type, except it merges the title into the body. This is useful for allowing single-line or multi-line blocks: 887 | 888 | // DefineHeader(Text): Reason 889 | // Reason: This is a simple reason. 890 | // Reason: This is a complex reason. 891 | // It is a multi-line explanation 892 | // about why this does what it does. 893 | // Refer to {{nonsense()}} for more info. 894 | 895 | Which outputs Markdown code that renders like: 896 | 897 | > **Reason:** 898 | > 899 | > This is a simple reason. 900 | > 901 | > **Reason:** 902 | > 903 | > This is a complex reason. 904 | > It is a multi-line explanation 905 | > about why this does what it does. 906 | > Refer to [nonsense()](foobar.scad#function-nonsense) for more info. 907 | > 908 | 909 | Headerless Block Type 910 | --------------------- 911 | 912 | The Headerless block header type is similar to the Generic type, except it merges the title into the body, and generates no header line. 913 | 914 | // DefineHeader(Headerless): Explanation 915 | // Explanation: This is a simple explanation. 916 | // Explanation: This is a complex explanation. 917 | // It is a multi-line explanation 918 | // about why this does what it does. 919 | // Refer to {{nonsense()}} for more info. 920 | 921 | Which outputs Markdown code that renders like: 922 | 923 | > This is a simple explanation. 924 | > 925 | > This is a complex explanation. 926 | > It is a multi-line explanation 927 | > about why this does what it does. 928 | > Refer to [nonsense()](foobar.scad#function-nonsense) for more info. 929 | > 930 | 931 | 932 | Label Block Type 933 | ---------------- 934 | 935 | The Label block header type takes just the title, and shows it with the header: 936 | 937 | // DefineHeader(Label): Regions 938 | // Regions: Antarctica, New Zealand 939 | // Regions: Europe, Australia 940 | 941 | Which outputs Markdown code that renders like: 942 | 943 | > **Regions:** Antarctica, New Zealand 944 | > **Regions:** Europe, Australia 945 | 946 | 947 | NumList Block Type 948 | ------------------ 949 | 950 | The NumList block header type takes both title and body lines, and outputs a 951 | numbered list block: 952 | 953 | // DefineHeader(NumList): Steps 954 | // Steps: How to handle being on fire. 955 | // Stop running around and panicing. 956 | // Drop to the ground. Refer to {{drop()}}. 957 | // Roll on the ground to smother the flames. 958 | 959 | Which outputs Markdown code that renders like: 960 | 961 | > **Steps:** How to handle being on fire. 962 | > 963 | > 1. Stop running around and panicing. 964 | > 2. Drop to the ground. Refer to [drop()](foobar.scad#function-drop). 965 | > 3. Roll on the ground to smother the flames. 966 | > 967 | 968 | 969 | BulletList Block Type 970 | --------------------- 971 | 972 | The BulletList block header type takes both title and body lines: 973 | 974 | // DefineHeader(BulletList): Side Effects 975 | // Side Effects: For Typical Uses 976 | // The variable {{$foo}} gets set. 977 | // The default for subsequent calls is updated. 978 | 979 | Which outputs Markdown code that renders like: 980 | 981 | > **Side Effects:** For Typical Uses 982 | > 983 | > - The variable [$foo](foobar.scad#function-foo) gets set. 984 | > - The default for subsequent calls is updated. 985 | > 986 | 987 | 988 | Table Block Type 989 | ---------------- 990 | 991 | The Table block header type outputs a header block with the title, followed by one or more tables. This is generally meant for definition lists. The header names are given as the `Headers=` option in the DefineHeader metadata. Header names are separated by `|` (vertical bar, or pipe) characters, and sets of headers (for multiple tables) are separated by `||` (two vertical bars). A header that starts with the `^` (hat, or circumflex) character, will cause the items in that column to be surrounded by \`foo\` literal markers. Cells in the body content are separated by `=` (equals signs): 992 | 993 | // DefineHeader(Table;Headers=^Link Name|Description): Anchors 994 | // Anchors: by Name 995 | // "link1" = Anchor for the joiner Located at the {{BACK}} side of the shape. 996 | // "a"/"b" = Anchor for the joiner Located at the {{FRONT}} side of the shape. 997 | 998 | Which outputs Markdown code that renders like: 999 | 1000 | > **Anchors:** by Name 1001 | > 1002 | > Link Name | Description 1003 | > -------------- | -------------------- 1004 | > `"link1"` | Anchor for the joiner at the [BACK](constants.scad#constant-back) side of the shape. 1005 | > `"a"` / `"b"` | Anchor for the joiner at the [FRONT](constants.scad#constant-front) side of the shape. 1006 | > 1007 | 1008 | You can have multiple subtables, separated by a line with only three dashes: `---`: 1009 | 1010 | // DefineHeader(Table;Headers=^Pos Arg|What it Does||^Names Arg|What it Does): Args 1011 | // Args: 1012 | // foo = The foo argument. 1013 | // bar = The bar argument. 1014 | // --- 1015 | // baz = The baz argument. 1016 | // qux = The baz argument. 1017 | 1018 | Which outputs Markdown code that renders like: 1019 | 1020 | > **Args:** 1021 | > 1022 | > Pos Arg | What it Does 1023 | > ----------- | -------------------- 1024 | > `foo` | The foo argument. 1025 | > `bar` | The bar argument. 1026 | > 1027 | > Named Arg | What it Does 1028 | > ----------- | -------------------- 1029 | > `baz` | The baz argument. 1030 | > `qux` | The qux argument. 1031 | > 1032 | 1033 | 1034 | Defaults Configuration 1035 | ====================== 1036 | 1037 | The `openscad_decsgen` script looks for an `.openscad_docsgen_rc` file in the source code directory it is run in. In that file, you can give a few defaults for what files will be processed, and where to save the generated documentation. 1038 | 1039 | --- 1040 | 1041 | To specify what directory to write the output documentation to, you can use the DocsDirectory block: 1042 | 1043 | DocsDirectory: wiki_dir 1044 | 1045 | --- 1046 | 1047 | To specify what target profile to output for, use the TargetProfile block. You must specify either `wiki` or `githubwiki` as the value: 1048 | 1049 | TargetProfile: githubwiki 1050 | 1051 | --- 1052 | 1053 | To specify what the project name is, use the ProjectName block, like this: 1054 | 1055 | ProjectName: My Project Name 1056 | 1057 | --- 1058 | 1059 | To specify what types of files will be generated, you can use the GenerateDocs block. You give it a comma separated list of docs file types like this: 1060 | 1061 | GenerateDocs: Files, ToC, Index, Topics, CheatSheet, Sidebar 1062 | 1063 | Where the valid docs file types are as follows: 1064 | 1065 | - `Files`: Generate a documentation file for each .scad input file. Generates Images. 1066 | - `ToC`: Generate a project-wide Table of Contents file. (TOC.md) 1067 | - `Index`: Generate an alphabetically sorted function/module/constants index file. (AlphaIndex.md) 1068 | - `Topics`: Generate a index file of topics, sorted alphabetically. (Topics.md) 1069 | - `CheatSheet`: Generate a CheatSheet summary of function/module Usages. (CheatSheet.md) 1070 | - `Cheat`: The same as `CheatSheet`. 1071 | - `Sidebar`: Generate a Wiki sidebar index of files. (\_Sidebar.md) 1072 | 1073 | --- 1074 | 1075 | To specify markdown text to put at the top of the _Sidebar.md file, you can use the SidebarHeader block. Any text given in the body will be inserted at the top of the generated sidebar. Lines with just a period (`.`) will be inserted as blank lines. 1076 | 1077 | SidebarHeader: 1078 | ## Header 1079 | . 1080 | This is *markdown* text that will be put at the top of the _Sidebar.md file. 1081 | You can include [Links](https://google.com) or even images. 1082 | 1083 | --- 1084 | 1085 | To specify markdown text to put between the index links and the file links of the _Sidebar.md file, you can use the SidebarMiddle block. Any text given in the body will be inserted verbatim. Lines with just a period (`.`) will be inserted as blank lines. 1086 | 1087 | SidebarMiddle: 1088 | ### Middle 1089 | . 1090 | This is *markdown* text that will be put between the index links and the file 1091 | links of the _Sidebar.md file. You can include [Links](https://google.com) or 1092 | even images. 1093 | 1094 | --- 1095 | 1096 | To specify markdown text to put at the bottom of the _Sidebar.md file, you can use the SidebarFooter block. Any text given in the body will be inserted verbatim at the bottom of the generated sidebar. Lines with just a period (`.`) will be inserted as blank lines. 1097 | 1098 | SidebarFooter: 1099 | ### Footer 1100 | . 1101 | This is *markdown* text that will be put at the bottom of the _Sidebar.md file. 1102 | You can include [Links](https://google.com) or even images. 1103 | 1104 | --- 1105 | 1106 | To specify the creation of Animated PNG files instead of Animated GIFs, you can use the UsePNGAnimations block. You give it a YES or NO value like: 1107 | 1108 | UsePNGAnimations: Yes 1109 | 1110 | --- 1111 | 1112 | To ignore specific files, to prevent generating documentation for them, you can use the IgnoreFiles block. Note that the commentline prefix is not needed in the configuration file: 1113 | 1114 | IgnoreFiles: 1115 | ignored1.scad 1116 | ignored2.scad 1117 | tmp_*.scad 1118 | 1119 | --- 1120 | 1121 | To prioritize the ordering of files when generating the Table of Contents and other indices, you can use the PrioritizeFiles block: 1122 | 1123 | PrioritizeFiles: 1124 | file1.scad 1125 | file2.scad 1126 | 1127 | --- 1128 | 1129 | You can define SynTags tags using the DefineSynTags block: 1130 | 1131 | DefineSynTags: 1132 | Geom = Can return geometry when called as a module. 1133 | Path = Can return a Path when called as a function. 1134 | VNF = Can return a VNF when called as a function. 1135 | 1136 | --- 1137 | 1138 | You can also use the DefineHeader block in the config file to make custom block headers: 1139 | 1140 | DefineHeader(Text;ItemOnly): Returns 1141 | DefineHeader(BulletList): Side Effects 1142 | DefineHeader(Table;Headers=^Anchor Name|Position): Extra Anchors 1143 | 1144 | 1145 | 1146 | -------------------------------------------------------------------------------- /openscad_docsgen/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | import os.path 5 | import re 6 | import sys 7 | import glob 8 | 9 | from .errorlog import ErrorLog, errorlog 10 | from .imagemanager import image_manager 11 | from .blocks import * 12 | from .logmanager import log_manager 13 | from .filehashes import FileHashes 14 | 15 | 16 | class OriginInfo: 17 | def __init__(self, file, line): 18 | self.file = file 19 | self.line = line 20 | 21 | @property 22 | def md_file(self): 23 | if self._md_in_links: 24 | return self.file+".md" 25 | return self.file 26 | 27 | 28 | class DocsGenParser(object): 29 | _header_pat = re.compile(r"^// ([A-Z][A-Za-z0-9_&-]*( ?[A-Z][A-Za-z0-9_&-]*)?)(\([^)]*\))?:( .*)?$") 30 | RCFILE = ".openscad_docsgen_rc" 31 | HASHFILE = ".source_hashes" 32 | 33 | def __init__(self, opts): 34 | self.opts = opts 35 | self.target = opts.target 36 | self.strict = opts.strict 37 | self.quiet = opts.quiet 38 | self.file_blocks = [] 39 | self.curr_file_block = None 40 | self.curr_section = None 41 | self.curr_item = None 42 | self.curr_parent = None 43 | self.ignored_file_pats = [] 44 | self.ignored_files = {} 45 | self.priority_files = [] 46 | self.priority_groups = [] 47 | self.items_by_name = {} 48 | self.definitions = {} 49 | self.defn_aliases = {} 50 | self.syntags_data = {} 51 | self.default_colorscheme = "Cornfield" 52 | 53 | sfx = self.target.get_suffix() 54 | self.TOCFILE = "TOC" + sfx 55 | self.TOPICFILE = "Topics" + sfx 56 | self.INDEXFILE = "AlphaIndex" + sfx 57 | self.GLOSSARYFILE = "Glossary" + sfx 58 | self.CHEATFILE = "CheatSheet" + sfx 59 | self.SIDEBARFILE = "_Sidebar" + sfx 60 | 61 | self._reset_header_defs() 62 | 63 | def _reset_header_defs(self): 64 | self.header_defs = { 65 | # BlockHeader: (parenttype, nodetype, extras, callback) 66 | 'Status': ( ItemBlock, LabelBlock, None, self._status_block_cb ), 67 | 'Alias': ( ItemBlock, LabelBlock, None, self._alias_block_cb ), 68 | 'Aliases': ( ItemBlock, LabelBlock, None, self._alias_block_cb ), 69 | 'Arguments': ( ItemBlock, TableBlock, ( 70 | ('^By Position', 'What it does'), 71 | ('^By Name', 'What it does') 72 | ), None 73 | ), 74 | } 75 | lines = [ 76 | "// DefineHeader(Headerless): Continues", 77 | "// DefineHeader(Text;ItemOnly): Description", 78 | "// DefineHeader(BulletList;ItemOnly): Usage", 79 | ] 80 | self.parse_lines(lines, src_file="Defaults") 81 | if os.path.exists(self.RCFILE): 82 | with open(self.RCFILE, "r") as f: 83 | lines = ["// " + line for line in f.readlines()] 84 | self.parse_lines(lines, src_file=self.RCFILE) 85 | 86 | def _status_block_cb(self, title, subtitle, body, origin, meta): 87 | self.curr_item.deprecated = "DEPRECATED" in subtitle 88 | 89 | def _alias_block_cb(self, title, subtitle, body, origin, meta): 90 | aliases = [x.strip() for x in subtitle.split(",")] 91 | self.curr_item.aliases.extend(aliases) 92 | for alias in aliases: 93 | self.items_by_name[alias] = self.curr_item 94 | 95 | def _validate_colorscheme(self, colorscheme): 96 | """Validate the color scheme against OpenSCAD's supported schemes.""" 97 | valid_schemes = [ 98 | 'Cornfield', 'Metallic', 'Sunset', 'Starnight', 'ClearSky', 'BeforeDawn', 'Nature', 'Daylight Gem', 'Nocturnal Gem', 99 | 'DeepOcean', 'Solarized', 'Tomorrow', 'Tomorrow Night' 100 | ] 101 | if colorscheme not in valid_schemes: 102 | errorlog.add_entry(self.RCFILE, 0, f"Invalid ColorScheme '{colorscheme}' in {self.RCFILE}. Using 'Cornfield'.", ErrorLog.WARNING) 103 | return 'Cornfield' 104 | return colorscheme 105 | 106 | def _skip_lines(self, lines, line_num=0): 107 | while line_num < len(lines): 108 | line = lines[line_num] 109 | if self.curr_item and not line.startswith("//"): 110 | self.curr_parent = self.curr_item.parent 111 | self.curr_item = None 112 | match = self._header_pat.match(line) 113 | if match: 114 | return line_num 115 | line_num += 1 116 | if self.curr_item: 117 | self.curr_parent = self.curr_item.parent 118 | self.curr_item = None 119 | return line_num 120 | 121 | def _files_prioritized(self): 122 | out = [] 123 | found = {} 124 | for pri_file in self.priority_files: 125 | for file_block in self.file_blocks: 126 | if file_block.subtitle == pri_file: 127 | found[pri_file] = True 128 | out.append(file_block) 129 | for file_block in self.file_blocks: 130 | if file_block.subtitle not in found: 131 | out.append(file_block) 132 | return out 133 | 134 | def _parse_meta_dict(self, meta): 135 | meta_dict = {} 136 | for part in meta.split(';'): 137 | if "=" in part: 138 | key, val = part.split('=',1) 139 | else: 140 | key, val = part, 1 141 | meta_dict[key] = val 142 | return meta_dict 143 | 144 | def _define_blocktype(self, title, meta): 145 | title = title.strip() 146 | parentspec = None 147 | meta = self._parse_meta_dict(meta) 148 | 149 | if "ItemOnly" in meta: 150 | parentspec = ItemBlock 151 | 152 | if "NumList" in meta: 153 | self.header_defs[title] = (parentspec, NumberedListBlock, None, None) 154 | elif "BulletList" in meta: 155 | self.header_defs[title] = (parentspec, BulletListBlock, None, None) 156 | elif "Table" in meta: 157 | if "Headers" not in meta: 158 | raise DocsGenException("DefineHeader", "Table type is missing Header= option, while declaring block:") 159 | hdr_meta = meta["Headers"].split("||") 160 | hdr_sets = [[x.strip() for x in hset.split("|")] for hset in hdr_meta] 161 | self.header_defs[title] = (parentspec, TableBlock, hdr_sets, None) 162 | elif "Example" in meta: 163 | self.header_defs[title] = (parentspec, ExampleBlock, None, None) 164 | elif "Figure" in meta: 165 | self.header_defs[title] = (parentspec, FigureBlock, None, None) 166 | elif "Label" in meta: 167 | self.header_defs[title] = (parentspec, LabelBlock, None, None) 168 | elif "Headerless" in meta: 169 | self.header_defs[title] = (parentspec, HeaderlessBlock, None, None) 170 | elif "Text" in meta: 171 | self.header_defs[title] = (parentspec, TextBlock, None, None) 172 | elif "Generic" in meta: 173 | self.header_defs[title] = (parentspec, GenericBlock, None, None) 174 | else: 175 | raise DocsGenException("DefineHeader", "Could not parse target block type, while declaring block:") 176 | 177 | def _check_filenode(self, title, origin): 178 | if not self.curr_file_block: 179 | raise DocsGenException(title, "Must declare File or Libfile block before declaring block:") 180 | 181 | def _parse_block(self, lines, line_num=0, src_file=None): 182 | line_num = self._skip_lines(lines, line_num) 183 | if line_num >= len(lines): 184 | return line_num 185 | hdr_line_num = line_num 186 | line = lines[line_num] 187 | match = self._header_pat.match(line) 188 | if not match: 189 | return line_num 190 | title = match.group(1) 191 | meta = match.group(3)[1:-1] if match.group(3) else "" 192 | subtitle = match.group(4).strip() if match.group(4) else "" 193 | body = [] 194 | unstripped_body = [] 195 | line_num += 1 196 | 197 | try: 198 | first_line = True 199 | indent = 2 200 | while line_num < len(lines): 201 | line = lines[line_num] 202 | if not line.startswith("//" + (" " * indent)): 203 | if line.startswith("// "): 204 | raise DocsGenException(title, "Body line has less indentation than first line, while declaring block:") 205 | break 206 | line = line[2:] 207 | if first_line: 208 | first_line = False 209 | indent = len(line) - len(line.lstrip()) 210 | line = line[indent:] 211 | unstripped_body.append(line.rstrip('\n')) 212 | body.append(line.rstrip()) 213 | line_num += 1 214 | 215 | parent = self.curr_parent 216 | origin = OriginInfo(src_file, hdr_line_num+1) 217 | if title == "DefineHeader": 218 | self._define_blocktype(subtitle, meta) 219 | elif title == "ColorScheme": 220 | if origin.file != self.RCFILE: 221 | raise DocsGenException(title, f"Block disallowed outside of {self.RCFILE} file:") 222 | if body: 223 | raise DocsGenException(title, "Body not supported, while declaring block:") 224 | if not subtitle: 225 | raise DocsGenException(title, "Must provide a color scheme (e.g., Tomorrow), while declaring block:") 226 | self.default_colorscheme = self._validate_colorscheme(subtitle.strip()) 227 | elif title == "IgnoreFiles": 228 | if origin.file != self.RCFILE: 229 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 230 | if subtitle: 231 | body.insert(0,subtitle) 232 | self.ignored_file_pats.extend([ 233 | fname.strip() for fname in body 234 | ]) 235 | self.ignored_files = {} 236 | for pat in self.ignored_file_pats: 237 | files = glob.glob(pat,recursive=True) 238 | for fname in files: 239 | self.ignored_files[fname] = True 240 | elif title == "PrioritizeFiles": 241 | if origin.file != self.RCFILE: 242 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 243 | if subtitle: 244 | body.insert(0,subtitle) 245 | self.priority_files = [x for line in body for x in glob.glob(line.strip())] 246 | elif title == "DocsDirectory": 247 | if origin.file != self.RCFILE: 248 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 249 | if body: 250 | raise DocsGenException(title, "Body not supported, while declaring block:") 251 | self.opts.docs_dir = subtitle.strip().rstrip("/") 252 | self.opts.update_target() 253 | elif title == "EnabledFeatures": 254 | self.opts.enabled_features = [item.strip() for item in subtitle.split(",") if item.strip()] 255 | self.opts.update_target() 256 | elif title == "UsePNGAnimations": 257 | if origin.file != self.RCFILE: 258 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 259 | if body: 260 | raise DocsGenException(title, "Body not supported, while declaring block:") 261 | self.opts.png_animation = (subtitle.strip().upper() in ["TRUE", "YES", "1"]) 262 | self.opts.update_target() 263 | elif title == "ProjectName": 264 | if origin.file != self.RCFILE: 265 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 266 | if body: 267 | raise DocsGenException(title, "Body not supported, while declaring block:") 268 | self.opts.project_name = subtitle.strip() 269 | self.opts.update_target() 270 | elif title == "TargetProfile": 271 | if origin.file != self.RCFILE: 272 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 273 | if body: 274 | raise DocsGenException(title, "Body not supported, while declaring block:") 275 | if not self.opts.set_target(subtitle.strip()): 276 | raise DocsGenException(title, "Body not supported, while declaring block:") 277 | self.opts.target_profile = subtitle.strip() 278 | self.opts.update_target() 279 | elif title == "GenerateDocs": 280 | if origin.file != self.RCFILE: 281 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 282 | if body: 283 | raise DocsGenException(title, "Body not supported, while declaring block:") 284 | if not ( 285 | self.opts.gen_files or 286 | self.opts.gen_toc or 287 | self.opts.gen_index or 288 | self.opts.gen_topics or 289 | self.opts.gen_cheat or 290 | self.opts.gen_sidebar or 291 | self.opts.gen_glossary 292 | ): 293 | # Only use default GeneratedDocs if the command-line doesn't specify any docs 294 | # types to generate. 295 | for part in subtitle.split(","): 296 | orig_part = part.strip() 297 | part = orig_part.upper() 298 | if part == "FILES": 299 | self.opts.gen_files = True 300 | elif part == "TOC": 301 | self.opts.gen_toc = True 302 | elif part == "INDEX": 303 | self.opts.gen_index = True 304 | elif part == "TOPICS": 305 | self.opts.gen_topics = True 306 | elif part in ["CHEAT", "CHEATSHEET"]: 307 | self.opts.gen_cheat = True 308 | elif part == "GLOSSARY": 309 | self.opts.gen_glossary = True 310 | elif part == "SIDEBAR": 311 | self.opts.gen_sidebar = True 312 | else: 313 | raise DocsGenException(title, 'Unknown type "{}", while declaring block:'.format(orig_part)) 314 | elif title == "SidebarHeader": 315 | if origin.file != self.RCFILE: 316 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 317 | body = unstripped_body 318 | if subtitle: 319 | body.insert(0,subtitle) 320 | body = [line[1:] if line.startswith(".") else line for line in body] 321 | self.opts.sidebar_header = body 322 | elif title == "SidebarMiddle": 323 | if origin.file != self.RCFILE: 324 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 325 | body = unstripped_body 326 | if subtitle: 327 | body.insert(0,subtitle) 328 | body = [line[1:] if line.startswith(".") else line for line in body] 329 | self.opts.sidebar_middle = body 330 | elif title == "SidebarFooter": 331 | if origin.file != self.RCFILE: 332 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 333 | body = unstripped_body 334 | if subtitle: 335 | body.insert(0,subtitle) 336 | body = [line[1:] if line.startswith(".") else line for line in body] 337 | self.opts.sidebar_footer = body 338 | elif title == "DefineSynTags": 339 | if origin.file != self.RCFILE: 340 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 341 | if subtitle: 342 | raise DocsGenException(title, "Subtitle not supported, while declaring block:") 343 | for line in body: 344 | if '=' not in line: 345 | raise DocsGenException(title, "Malformed tag definition '{}' while declaring block:".format(line)) 346 | tag, text = [x.strip() for x in line.split("=",1)] 347 | self.syntags_data[tag] = text 348 | elif title == "vim" or title == "emacs": 349 | pass # Ignore vim and emacs modelines 350 | elif title in ["File", "LibFile"]: 351 | if self.curr_file_block: 352 | raise DocsGenException(title, "File or Libfile must be the first block specified, and must be specified at most once. Encountered while declaring block:") 353 | self.curr_file_block = FileBlock(title, subtitle, body, origin) 354 | self.curr_section = None 355 | self.curr_subsection = None 356 | self.curr_parent = self.curr_file_block 357 | self.file_blocks.append(self.curr_file_block) 358 | elif not self.curr_file_block and self.strict: 359 | raise DocsGenException(title, "Must declare File or LibFile block before declaring block:") 360 | 361 | elif title == "Section": 362 | self._check_filenode(title, origin) 363 | self.curr_section = SectionBlock(title, subtitle, body, origin, parent=self.curr_file_block) 364 | self.curr_subsection = None 365 | self.curr_parent = self.curr_section 366 | elif title == "Subsection": 367 | if not self.curr_section: 368 | raise DocsGenException(title, "Must declare a Section before declaring block:") 369 | if not subtitle: 370 | raise DocsGenException(title, "Must provide a subtitle when declaring block:") 371 | self.curr_subsection = SubsectionBlock(title, subtitle, body, origin, parent=self.curr_section) 372 | self.curr_parent = self.curr_subsection 373 | elif title == "Includes": 374 | self._check_filenode(title, origin) 375 | IncludesBlock(title, subtitle, body, origin, parent=self.curr_file_block) 376 | elif title == "FileSummary": 377 | if not subtitle: 378 | raise DocsGenException(title, "Must provide a subtitle when declaring block:") 379 | self._check_filenode(title, origin) 380 | self.curr_file_block.summary = subtitle.strip() 381 | elif title == "FileGroup": 382 | if not subtitle: 383 | raise DocsGenException(title, "Must provide a subtitle when declaring block:") 384 | self._check_filenode(title, origin) 385 | self.curr_file_block.group = subtitle.strip() 386 | elif title == "FileFootnotes": 387 | if not subtitle: 388 | raise DocsGenException(title, "Must provide a subtitle when declaring block:") 389 | self._check_filenode(title, origin) 390 | self.curr_file_block.footnotes = [] 391 | for part in subtitle.split(";"): 392 | fndata = [x.strip() for x in part.strip().split('=',1)] 393 | fndata.append(origin) 394 | self.curr_file_block.footnotes.append(fndata) 395 | elif title == "CommonCode": 396 | self._check_filenode(title, origin) 397 | self.curr_file_block.common_code.extend(body) 398 | elif title == "Definitions": 399 | self._check_filenode(title, origin) 400 | block = DefinitionsBlock(title, subtitle, body, origin, parent=parent) 401 | for main_term, info in block.definitions.items(): 402 | terms, defn = info 403 | for term in terms: 404 | if term.lower() in self.definitions or term in self.defn_aliases: 405 | raise DocsGenException(title, 'Term "{}" re-defined, while declaring block:'.format(term)) 406 | self.definitions[main_term.lower()] = (terms, defn) 407 | for term in terms[1:]: 408 | self.defn_aliases[term.lower()] = main_term 409 | elif title == "Figure": 410 | self._check_filenode(title, origin) 411 | FigureBlock(title, subtitle, body, origin, verbose=self.opts.verbose, parent=parent, enabled_features=self.opts.enabled_features, meta=meta, use_apngs=self.opts.png_animation) 412 | elif title == "Example": 413 | if self.curr_item: 414 | ExampleBlock(title, subtitle, body, origin, verbose=self.opts.verbose, parent=parent, enabled_features=self.opts.enabled_features, meta=meta, use_apngs=self.opts.png_animation) 415 | elif title == "Figures": 416 | self._check_filenode(title, origin) 417 | for lnum, line in enumerate(body): 418 | FigureBlock("Figure", subtitle, [line], origin, verbose=self.opts.verbose, parent=parent, enabled_features=self.opts.enabled_features, meta=meta, use_apngs=self.opts.png_animation) 419 | subtitle = "" 420 | elif title == "Examples": 421 | if self.curr_item: 422 | for lnum, line in enumerate(body): 423 | ExampleBlock("Example", subtitle, [line], origin, verbose=self.opts.verbose, enabled_features=self.opts.enabled_features, parent=parent, meta=meta, use_apngs=self.opts.png_animation) 424 | subtitle = "" 425 | elif title == "Log": 426 | if self.curr_item: 427 | LogBlock(title, subtitle.strip(), body, origin, parent=parent, meta=meta) 428 | elif title in self.header_defs: 429 | parcls, cls, data, cb = self.header_defs[title] 430 | if not parcls or isinstance(self.curr_parent, parcls): 431 | if cls in (GenericBlock, LabelBlock, TextBlock, HeaderlessBlock, NumberedListBlock, BulletListBlock): 432 | cls(title, subtitle, body, origin, parent=parent) 433 | elif cls == TableBlock: 434 | cls(title, subtitle, body, origin, parent=parent, header_sets=data) 435 | elif cls in (FigureBlock, ExampleBlock): 436 | cls(title, subtitle, body, origin, parent=parent, meta=meta, use_apngs=self.opts.png_animation) 437 | if cb: 438 | cb(title, subtitle, body, origin, meta) 439 | 440 | elif title in ["Constant", "Function", "Module", "Function&Module"]: 441 | self._check_filenode(title, origin) 442 | if not self.curr_section: 443 | self.curr_section = SectionBlock("Section", "", [], origin, parent=self.curr_file_block) 444 | parent = self.curr_parent = self.curr_section 445 | if subtitle in self.items_by_name: 446 | prevorig = self.items_by_name[subtitle].origin 447 | msg = "Previous declaration of `{}` at {}:{}, Redeclared:".format(subtitle, prevorig.file, prevorig.line) 448 | raise DocsGenException(title, msg) 449 | item = ItemBlock(title, subtitle, body, origin, parent=parent) 450 | self.items_by_name[subtitle] = item 451 | self.curr_item = item 452 | self.curr_parent = item 453 | elif title == "Synopsis": 454 | if self.curr_item: 455 | SynopsisBlock(title, subtitle, body, origin, parent=parent) 456 | elif title == "SynTags": 457 | if self.curr_item: 458 | SynTagsBlock(title, subtitle, body, origin, parent=parent, syntags_data=self.syntags_data) 459 | elif title == "Topics": 460 | if self.curr_item: 461 | TopicsBlock(title, subtitle, body, origin, parent=parent) 462 | elif title == "See Also": 463 | if self.curr_item: 464 | SeeAlsoBlock(title, subtitle, body, origin, parent=parent) 465 | else: 466 | raise DocsGenException(title, "Unrecognized block:") 467 | 468 | if line_num >= len(lines) or not lines[line_num].startswith("//"): 469 | if self.curr_item: 470 | self.curr_parent = self.curr_item.parent 471 | self.curr_item = None 472 | line_num = self._skip_lines(lines, line_num) 473 | 474 | except DocsGenException as e: 475 | errorlog.add_entry(origin.file, origin.line, str(e), ErrorLog.FAIL) 476 | 477 | return line_num 478 | 479 | def get_indexed_names(self): 480 | """Returns the list of all indexable function/module/constants by name, in alphabetical order. 481 | """ 482 | lst = sorted(self.items_by_name.keys()) 483 | for item in lst: 484 | yield item 485 | 486 | def get_indexed_data(self, name): 487 | """Given the name of an indexable function/module/constant, returns the parsed data dictionary for that item's documentation. 488 | 489 | Example Results 490 | --------------- 491 | { 492 | "name": "Function&Module", 493 | "subtitle": "foobar()", 494 | "body": [], 495 | "file": "foobar.scad", 496 | "line": 23, 497 | "topics": ["Testing", "Metasyntactic"], 498 | "aliases": ["foob()", "feeb()"], 499 | "see_also": ["barbaz()", "bazqux()"], 500 | "synopsis": "This function does bar.", 501 | "syntags": { 502 | "VNF": "Returns a VNF when called as a function.", 503 | "Geom": "Returns Geometry when called as a module." 504 | }, 505 | "usages": [ 506 | { 507 | "subtitle": "As function", 508 | "body": [ 509 | "val = foobar(a, b, );", 510 | "list = foobar(d, b=);" 511 | ] 512 | }, { 513 | "subtitle": "As module", 514 | "body": [ 515 | "foobar(a, b, );", 516 | "foobar(d, b=);" 517 | ] 518 | } 519 | ], 520 | "description": [ 521 | "When called as a function, this returns the foo of bar.", 522 | "When called as a module, renders a foo as modified by bar." 523 | ], 524 | "arguments": [ 525 | "a = The a argument.", 526 | "b = The b argument.", 527 | "c = The c argument.", 528 | "d = The d argument." 529 | ], 530 | "examples": [ 531 | [ 532 | "foobar(5, 7)" 533 | ], [ 534 | "x = foobar(5, 7);", 535 | "echo(x);" 536 | ] 537 | ] 538 | "children": [ 539 | { 540 | "name": "Extra Anchors", 541 | "subtitle": "", 542 | "body": [ 543 | "\"fee\" = Anchors at the fee position.", 544 | "\"fie\" = Anchors at the fie position." 545 | ] 546 | } 547 | ] 548 | } 549 | """ 550 | if name in self.items_by_name: 551 | return self.items_by_name[name].get_data() 552 | return {} 553 | 554 | def get_all_data(self): 555 | """Gets all the documentation data parsed so far. 556 | 557 | Sample Results 558 | ---------- 559 | [ 560 | { 561 | "name": "LibFile", 562 | "subtitle":"foobar.scad", 563 | "body": [ 564 | "This is the first line of the LibFile body.", 565 | "This is the second line of the LibFile body." 566 | ], 567 | "includes": [ 568 | "include ", 569 | "include " 570 | ], 571 | "commoncode": [ 572 | "$fa = 2;", 573 | "$fs = 2;" 574 | ], 575 | "children": [ 576 | { 577 | "name": "Section", 578 | "subtitle": "Metasyntactical Calls", // If subtitle is "", section is just a placeholder. 579 | "body": [ 580 | "This is the first line of the body of the Section.", 581 | "This is the second line of the body of the Section." 582 | ], 583 | "children": [ 584 | { 585 | "name": "Function&Module", 586 | "subtitle": "foobar()", 587 | "body": [], 588 | "file": "foobar.scad", 589 | "line": 23, 590 | "topics": ["Testing", "Metasyntactic"], 591 | "aliases": ["foob()", "feeb()"], 592 | "see_also": ["barbaz()", "bazqux()"], 593 | "synopsis": "This function does bar.", 594 | "syntags": { 595 | "VNF": "Returns a VNF when called as a function.", 596 | "Geom": "Returns Geometry when called as a module." 597 | }, 598 | "usages": [ 599 | { 600 | "subtitle": "As function", 601 | "body": [ 602 | "val = foobar(a, b, );", 603 | "list = foobar(d, b=);" 604 | ] 605 | }, { 606 | "subtitle": "As module", 607 | "body": [ 608 | "foobar(a, b, );", 609 | "foobar(d, b=);" 610 | ] 611 | } 612 | ], 613 | "description": [ 614 | "When called as a function, this returns the foo of bar.", 615 | "When called as a module, renders a foo as modified by bar." 616 | ], 617 | "arguments": [ 618 | "a = The a argument.", 619 | "b = The b argument.", 620 | "c = The c argument.", 621 | "d = The d argument." 622 | ], 623 | "examples": [ 624 | [ 625 | "foobar(5, 7)" 626 | ], 627 | [ 628 | "x = foobar(5, 7);", 629 | "echo(x);" 630 | ], 631 | // ... Next Example 632 | ] 633 | "children": [ 634 | { 635 | "name": "Extra Anchors", 636 | "subtitle": "", 637 | "body": [ 638 | "\"fee\" = Anchors at the fee position.", 639 | "\"fie\" = Anchors at the fie position." 640 | ] 641 | } 642 | ] 643 | }, 644 | // ... next function/module/constant 645 | ] 646 | }, 647 | // ... next section 648 | ] 649 | }, 650 | // ... next file 651 | ] 652 | """ 653 | return [ 654 | fblock.get_data() 655 | for fblock in self.file_blocks 656 | ] 657 | 658 | def parse_lines(self, lines, line_num=0, src_file=None): 659 | """Parses the given list of strings for documentation comments. 660 | 661 | Parameters 662 | ---------- 663 | lines : list of str 664 | The list of strings to parse for documentation comments. 665 | line_num : int 666 | The current index into the list of strings of the current line to parse. 667 | src_file : str 668 | The name of the source file that this is from. This is used just for error reporting. 669 | If true, generates images for example scripts, by running them in OpenSCAD. 670 | """ 671 | while line_num < len(lines): 672 | line_num = self._parse_block(lines, line_num, src_file=src_file) 673 | 674 | def parse_file(self, filename, commentless=False): 675 | """Parses the given file for documentation comments. 676 | 677 | Parameters 678 | ---------- 679 | filename : str 680 | The name of the file to parse documentaiton comments from. 681 | commentless : bool 682 | If true, treat every line of the file as if it starts with '// '. This is used for reading docsgen config files. 683 | """ 684 | if filename in self.ignored_files: 685 | return 686 | if not self.quiet: 687 | print(" {}".format(filename), end='') 688 | sys.stdout.flush() 689 | self.curr_file_block = None 690 | self.curr_section = None 691 | self._reset_header_defs() 692 | with open(filename, "r") as f: 693 | if commentless: 694 | lines = ["// " + line for line in f.readlines()] 695 | else: 696 | lines = f.readlines() 697 | self.parse_lines(lines, src_file=filename) 698 | 699 | def parse_files(self, filenames, commentless=False): 700 | """Parses all of the given files for documentation comments. 701 | 702 | Parameters 703 | ---------- 704 | filenames : list of str 705 | The list of filenames to parse documentaiton comments from. 706 | commentless : bool 707 | If true, treat every line of the files as if they starts with '// '. This is used for reading docsgen config files. 708 | """ 709 | if not self.quiet: 710 | print("Parsing...") 711 | print(" ", end='') 712 | col = 1 713 | for filename in filenames: 714 | if filename in self.ignored_files: 715 | continue 716 | flen = len(filename) + 1 717 | if col > 1 and flen + col >= 79: 718 | print("") 719 | print(" ", end='') 720 | col = 1 721 | self.parse_file(filename, commentless=commentless) 722 | col = col + flen 723 | for key, info in self.definitions.items(): 724 | keys, defn = info 725 | blk = self.file_blocks[0] 726 | defn = blk.parse_links(defn, self, self.target, html=False) 727 | self.definitions[key.lower()] = (keys, defn) 728 | if not self.quiet: 729 | print("") 730 | 731 | def dump_tree(self, nodes, pfx="", maxdepth=6): 732 | """Dumps debug info to stdout for parsed documentation subtree.""" 733 | if maxdepth <= 0 or not nodes: 734 | return 735 | for node in nodes: 736 | print("{}{}".format(pfx,node)) 737 | for line in node.body: 738 | print(" {}{}".format(pfx,line)) 739 | self.dump_tree(node.children, pfx=pfx+" ", maxdepth=maxdepth-1) 740 | 741 | def dump_full_tree(self): 742 | """Dumps debug info to stdout for all parsed documentation.""" 743 | self.dump_tree(self.file_blocks) 744 | 745 | def write_docs_files(self): 746 | """Generates the docs files for each source file that has been parsed. 747 | """ 748 | target = self.opts.target 749 | if self.opts.test_only: 750 | for fblock in sorted(self.file_blocks, key=lambda x: x.subtitle.strip()): 751 | lines = fblock.get_file_lines(self, target) 752 | image_manager.process_requests(test_only=True) 753 | return 754 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 755 | filehashes = FileHashes(os.path.join(target.docs_dir, self.HASHFILE)) 756 | for fblock in sorted(self.file_blocks, key=lambda x: x.subtitle.strip()): 757 | outfile = os.path.join(target.docs_dir, fblock.origin.file+target.get_suffix()) 758 | if not self.quiet: 759 | print("Writing {}...".format(outfile)) 760 | outdir = os.path.dirname(outfile) 761 | if not os.path.exists(outdir): 762 | os.makedirs(outdir, mode=0o744, exist_ok=True) 763 | out = fblock.get_file_lines(self, target) 764 | out = target.postprocess(out) 765 | with open(outfile,"w") as f: 766 | for line in out: 767 | f.write(line + "\n") 768 | if self.opts.gen_imgs: 769 | filename = fblock.subtitle.strip() 770 | has_changed = filehashes.is_changed(filename) 771 | if self.opts.force or has_changed: 772 | image_manager.process_requests(test_only=False) 773 | image_manager.purge_requests() 774 | if errorlog.file_has_errors(filename): 775 | filehashes.invalidate(filename) 776 | filehashes.save() 777 | 778 | def write_toc_file(self): 779 | """Generates the table-of-contents TOC file from the parsed documentation""" 780 | target = self.opts.target 781 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 782 | prifiles = self._files_prioritized() 783 | groups = [] 784 | for fblock in prifiles: 785 | if fblock.group and fblock.group not in groups: 786 | groups.append(fblock.group) 787 | for fblock in prifiles: 788 | if not fblock.group and fblock.group not in groups: 789 | groups.append(fblock.group) 790 | 791 | footmarks = [] 792 | footnotes = {} 793 | out = target.header("Table of Contents") 794 | out.extend(target.header("List of Files", lev=target.SECTION)) 795 | for group in groups: 796 | out.extend(target.block_header(group if group else "Miscellaneous")) 797 | out.extend(target.bullet_list_start()) 798 | for fnum, fblock in enumerate(prifiles): 799 | if fblock.group != group: 800 | continue 801 | file = fblock.subtitle 802 | anch = target.header_link("{}. {}".format(fnum+1, file)) 803 | link = target.get_link(file, anchor=anch, literalize=False) 804 | filelink = target.get_link("docs", file=file, literalize=False) 805 | tags = {tag: text for tag, text, origin in fblock.footnotes} 806 | marks = target.mouseover_tags(tags, "#file-footnotes") 807 | out.extend(target.bullet_list_item("{} ({}){}".format(link, filelink, marks))) 808 | out.append(fblock.summary) 809 | for mark, note, origin in fblock.footnotes: 810 | try: 811 | if mark not in footmarks: 812 | footmarks.append(mark) 813 | if mark not in footnotes: 814 | footnotes[mark] = note 815 | elif note != footnotes[mark]: 816 | raise DocsGenException("FileFootnotes", 'Footnote "{}" conflicts with previous definition "{}", while declaring block:'.format(note, footnotes[mark])) 817 | except DocsGenException as e: 818 | errorlog.add_entry(origin.file, origin.line, str(e), ErrorLog.FAIL) 819 | out.extend(target.bullet_list_end()) 820 | 821 | if footmarks: 822 | out.append("") 823 | out.extend(target.header("File Footnotes:", lev=target.SUBSECTION)) 824 | for mark in footmarks: 825 | out.append("{} = {} ".format(mark, note)) 826 | out.append("") 827 | 828 | for fnum, fblock in enumerate(prifiles): 829 | out.extend(fblock.get_tocfile_lines(self, self.opts.target, n=fnum+1, currfile=self.TOCFILE)) 830 | 831 | out = target.postprocess(out) 832 | outfile = os.path.join(target.docs_dir, self.TOCFILE) 833 | if not self.quiet: 834 | print("Writing {}...".format(outfile)) 835 | with open(outfile, "w") as f: 836 | for line in out: 837 | f.write(line + "\n") 838 | 839 | def write_glossary_file(self): 840 | """Generates the Glossary file from the parsed documentation.""" 841 | target = self.opts.target 842 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 843 | defs = {key: info[1] for key, info in self.definitions.items()} 844 | sorted_words = sorted(list(defs.keys()), key=lambda v: v.upper()) 845 | ltrs_found = {} 846 | for word in sorted_words: 847 | ltr = word[0].upper() 848 | ltrs_found[ltr] = 1 849 | ltrs_found = sorted(list(ltrs_found.keys())) 850 | 851 | out = [] 852 | out = target.header("Glossary") 853 | out.extend(target.markdown_block([ 854 | "Definitions of various Words and terms." 855 | ])) 856 | out.extend(target.markdown_block([ 857 | " ".join( 858 | target.get_link(ltr.upper(), anchor=ltr.lower(), literalize=False) 859 | for ltr in ltrs_found 860 | ) 861 | ])) 862 | out.extend(target.horizontal_rule()) 863 | old_ltr = '' 864 | for word in sorted_words: 865 | ltr = word[0].upper() 866 | if old_ltr != ltr: 867 | out.extend(target.header(ltr.upper(), lev=2)) 868 | old_ltr = ltr 869 | defn = defs[word.lower()] 870 | out.extend(target.header(word.title(), lev=3)) 871 | out.extend(target.markdown_block([defn])) 872 | out = target.postprocess(out) 873 | outfile = os.path.join(target.docs_dir, self.GLOSSARYFILE) 874 | if not self.quiet: 875 | print("Writing {}...".format(outfile)) 876 | with open(outfile, "w") as f: 877 | for line in out: 878 | f.write(line + "\n") 879 | 880 | def write_topics_file(self): 881 | """Generates the Topics file from the parsed documentation.""" 882 | target = self.opts.target 883 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 884 | index_by_letter = {} 885 | for file_block in self.file_blocks: 886 | for section in file_block.children: 887 | if not isinstance(section,SectionBlock): 888 | continue 889 | for item in section.children: 890 | if not isinstance(item,ItemBlock): 891 | continue 892 | names = [item.subtitle] 893 | names.extend(item.aliases) 894 | for topic in item.topics: 895 | ltr = "0" if not topic[0].isalpha() else topic[0].upper() 896 | if ltr not in index_by_letter: 897 | index_by_letter[ltr] = {} 898 | if topic not in index_by_letter[ltr]: 899 | index_by_letter[ltr][topic] = [] 900 | for name in names: 901 | index_by_letter[ltr][topic].append( (name, item) ) 902 | ltrs_found = sorted(index_by_letter.keys()) 903 | out = target.header("Topic Index") 904 | out.extend(target.markdown_block([ 905 | "An index of topics, with related functions, modules, and constants." 906 | ])) 907 | for ltr in ltrs_found: 908 | out.extend( 909 | target.markdown_block([ 910 | "{}: {}".format( 911 | target.bold(ltr), 912 | ", ".join( 913 | target.get_link( 914 | target.escape_entities(topic), 915 | anchor=target.header_link(topic), 916 | literalize=False 917 | ) 918 | for topic in sorted(index_by_letter[ltr].keys()) 919 | ) 920 | ) 921 | ]) 922 | ) 923 | for ltr in ltrs_found: 924 | topics = sorted(index_by_letter[ltr].keys()) 925 | for topic in topics: 926 | itemlist = index_by_letter[ltr][topic] 927 | out.extend(target.header(topic, lev=target.ITEM)) 928 | out.extend(target.bullet_list_start()) 929 | sorted_items = sorted(itemlist, key=lambda x: x[0].lower()) 930 | for name, item in sorted_items: 931 | out.extend( 932 | target.bullet_list_item( 933 | item.get_index_line(self, target, self.TOPICFILE) 934 | ) 935 | ) 936 | out.extend(target.bullet_list_end()) 937 | 938 | out = target.postprocess(out) 939 | outfile = os.path.join(target.docs_dir, self.TOPICFILE) 940 | if not self.quiet: 941 | print("Writing {}...".format(outfile)) 942 | with open(outfile, "w") as f: 943 | for line in out: 944 | f.write(line + "\n") 945 | 946 | def write_index_file(self): 947 | """Generates the alphabetical function/module/constant AlphaIndex file from the parsed documentation.""" 948 | target = self.opts.target 949 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 950 | unsorted_items = [] 951 | for file_block in self.file_blocks: 952 | for sect in file_block.get_children_by_title("Section"): 953 | items = [ 954 | item for item in sect.children 955 | if isinstance(item, ItemBlock) 956 | ] 957 | for item in items: 958 | names = [item.subtitle] 959 | names.extend(item.aliases) 960 | for name in names: 961 | unsorted_items.append( (name, item) ) 962 | sorted_items = sorted(unsorted_items, key=lambda x: x[0].lower()) 963 | index_by_letter = {} 964 | for name, item in sorted_items: 965 | ltr = "0" if not name[0].isalpha() else name[0].upper() 966 | if ltr not in index_by_letter: 967 | index_by_letter[ltr] = [] 968 | index_by_letter[ltr].append( (name, item ) ) 969 | ltrs_found = sorted(index_by_letter.keys()) 970 | out = target.header("Alphabetical Index") 971 | out.extend(target.markdown_block([ 972 | "An index of Functions, Modules, and Constants by name.", 973 | ])) 974 | out.extend(target.markdown_block([ 975 | " ".join( 976 | target.get_link(ltr, anchor=ltr.lower(), literalize=False) 977 | for ltr in ltrs_found 978 | ) 979 | ])) 980 | for ltr in ltrs_found: 981 | items = [ 982 | item.get_index_line(self, target, self.INDEXFILE) 983 | for name, item in index_by_letter[ltr] 984 | ] 985 | out.extend(target.header(ltr, lev=target.SUBSECTION)) 986 | out.extend(target.bullet_list(items)) 987 | 988 | out = target.postprocess(out) 989 | outfile = os.path.join(target.docs_dir, self.INDEXFILE) 990 | if not self.quiet: 991 | print("Writing {}...".format(outfile)) 992 | with open(outfile, "w") as f: 993 | for line in out: 994 | f.write(line + "\n") 995 | 996 | def write_cheatsheet_file(self): 997 | """Generates the CheatSheet file from the parsed documentation.""" 998 | target = self.opts.target 999 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 1000 | if target.project_name is None: 1001 | title = "Cheat Sheet" 1002 | else: 1003 | title = "{} Cheat Sheet".format(target.project_name) 1004 | out = target.header(title) 1005 | pri_blocks = self._files_prioritized() 1006 | for file_block in pri_blocks: 1007 | out.extend(file_block.get_cheatsheet_lines(self, self.opts.target)) 1008 | 1009 | out = target.postprocess(out) 1010 | outfile = os.path.join(target.docs_dir, self.CHEATFILE) 1011 | if not self.quiet: 1012 | print("Writing {}...".format(outfile)) 1013 | with open(outfile, "w") as f: 1014 | for line in out: 1015 | f.write(line + "\n") 1016 | 1017 | def write_sidebar_file(self): 1018 | """Generates the _Sidebar index of files from the parsed documentation""" 1019 | target = self.opts.target 1020 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 1021 | prifiles = self._files_prioritized() 1022 | groups = [] 1023 | for fblock in prifiles: 1024 | if fblock.group and fblock.group not in groups: 1025 | groups.append(fblock.group) 1026 | for fblock in prifiles: 1027 | if not fblock.group and fblock.group not in groups: 1028 | groups.append(fblock.group) 1029 | 1030 | footmarks = [] 1031 | footnotes = {} 1032 | out = [] 1033 | if self.opts.sidebar_header: 1034 | out.extend(self.opts.sidebar_header) 1035 | if self.opts.gen_toc: 1036 | out.extend(target.line_with_break(target.get_link("Table of Contents", file="TOC", literalize=False))) 1037 | if self.opts.gen_index: 1038 | out.extend(target.line_with_break(target.get_link("Function Index", file="AlphaIndex", literalize=False))) 1039 | if self.opts.gen_topics: 1040 | out.extend(target.line_with_break(target.get_link("Topics Index", file="Topics", literalize=False))) 1041 | if self.opts.gen_glossary: 1042 | out.extend(target.line_with_break(target.get_link("Glossary", file="Glossary", literalize=False))) 1043 | if self.opts.gen_cheat: 1044 | out.extend(target.line_with_break(target.get_link("Cheat Sheet", file="CheatSheet", literalize=False))) 1045 | if self.opts.sidebar_middle: 1046 | out.extend(self.opts.sidebar_middle) 1047 | out.extend(target.paragraph()) 1048 | out.extend(target.header("List of Files:", lev=target.SUBSECTION)) 1049 | for group in groups: 1050 | out.extend(target.block_header(group if group else "Miscellaneous")) 1051 | out.extend(target.bullet_list_start()) 1052 | for fnum, fblock in enumerate(prifiles): 1053 | if fblock.group != group: 1054 | continue 1055 | file = fblock.subtitle 1056 | link = target.get_link(file, file=file, literalize=False) 1057 | for mark, note, origin in fblock.footnotes: 1058 | try: 1059 | if mark not in footmarks: 1060 | footmarks.append(mark) 1061 | if mark not in footnotes: 1062 | footnotes[mark] = note 1063 | elif note != footnotes[mark]: 1064 | raise DocsGenException("FileFootnotes", 'Footnote "{}" conflicts with previous definition "{}", while declaring block:'.format(note, footnotes[mark])) 1065 | except DocsGenException as e: 1066 | errorlog.add_entry(origin.file, origin.line, str(e), ErrorLog.FAIL) 1067 | tags = {tag: text for tag, text, origin in fblock.footnotes} 1068 | marks = target.mouseover_tags(tags, "#footnotes") 1069 | out.extend(target.bullet_list_item("{}{}".format(link, marks))) 1070 | out.extend(target.bullet_list_end()) 1071 | if footmarks: 1072 | out.append("") 1073 | out.extend(target.header("Footnotes:", lev=target.SUBSECTION)) 1074 | for mark in footmarks: 1075 | out.append("{} = {} ".format(mark, note)) 1076 | if self.opts.sidebar_footer: 1077 | out.extend(self.opts.sidebar_footer) 1078 | 1079 | out = target.postprocess(out) 1080 | outfile = os.path.join(target.docs_dir, self.SIDEBARFILE) 1081 | if not self.quiet: 1082 | print("Writing {}...".format(outfile)) 1083 | with open(outfile, "w") as f: 1084 | for line in out: 1085 | f.write(line + "\n") 1086 | 1087 | 1088 | 1089 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 1090 | --------------------------------------------------------------------------------