├── 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 | '
'.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}{htag}>'
64 | elif '#' in file:
65 | fmt = ' <{htag} title="{text}">[{abbr}]({link}){htag}>'
66 | else:
67 | fmt = ' <{htag} title="{text}">[{abbr}]({link}#{linktag}){htag}>'
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 | ''.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("".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 |
--------------------------------------------------------------------------------