├── .gitignore ├── LICENSE ├── README.rst ├── WRITING_DOCS.md ├── openscad_docsgen ├── __init__.py ├── blocks.py ├── errorlog.py ├── filehashes.py ├── imagemanager.py ├── mdimggen.py ├── parser.py ├── target.py ├── target_githubwiki.py ├── target_wiki.py └── utils.py ├── pyproject.toml └── setup.py /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/__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 | 16 | 17 | class Options(object): 18 | def __init__(self, args): 19 | self.files = args.srcfiles 20 | self.target_profile = args.target_profile 21 | self.project_name = args.project_name 22 | self.docs_dir = args.docs_dir.rstrip("/") 23 | self.quiet = args.quiet 24 | self.force = args.force 25 | self.strict = args.strict 26 | self.test_only = args.test_only 27 | self.gen_imgs = not args.no_images 28 | self.gen_files = args.gen_files 29 | self.gen_toc = args.gen_toc 30 | self.gen_index = args.gen_index 31 | self.gen_topics = args.gen_topics 32 | self.gen_glossary = args.gen_glossary 33 | self.gen_cheat = args.gen_cheat 34 | self.gen_sidebar = args.gen_sidebar 35 | self.report = args.report 36 | self.dump_tree = args.dump_tree 37 | self.png_animation = args.png_animation 38 | self.sidebar_header = [] 39 | self.sidebar_middle = [] 40 | self.sidebar_footer = [] 41 | self.update_target() 42 | 43 | def set_target(self, targ): 44 | if targ not in target_classes: 45 | return False 46 | self.target_profile = targ 47 | return True 48 | 49 | def update_target(self): 50 | self.target = target_classes[self.target_profile]( 51 | project_name=self.project_name, 52 | docs_dir=self.docs_dir 53 | ) 54 | 55 | def processFiles(opts): 56 | docsgen = DocsGenParser(opts) 57 | # DocsGenParser may change opts settings, based on the _rc file. 58 | 59 | if not opts.files: 60 | opts.files = glob.glob("*.scad") 61 | elif platform.system() == 'Windows': 62 | opts.files = [file for src_file in opts.files for file in glob.glob(src_file)] 63 | 64 | fail = False 65 | for infile in opts.files: 66 | if not os.path.exists(infile): 67 | print("{} does not exist.".format(infile)) 68 | fail = True 69 | elif not os.path.isfile(infile): 70 | print("{} is not a file.".format(infile)) 71 | fail = True 72 | elif not os.access(infile, os.R_OK): 73 | print("{} is not readable.".format(infile)) 74 | fail = True 75 | if fail: 76 | sys.exit(-1) 77 | 78 | docsgen.parse_files(opts.files, False) 79 | 80 | if opts.dump_tree: 81 | docsgen.dump_full_tree() 82 | 83 | if opts.gen_files or opts.test_only: 84 | docsgen.write_docs_files() 85 | if opts.gen_toc: 86 | docsgen.write_toc_file() 87 | if opts.gen_index: 88 | docsgen.write_index_file() 89 | if opts.gen_topics: 90 | docsgen.write_topics_file() 91 | if opts.gen_glossary: 92 | docsgen.write_glossary_file() 93 | if opts.gen_cheat: 94 | docsgen.write_cheatsheet_file() 95 | if opts.gen_sidebar: 96 | docsgen.write_sidebar_file() 97 | 98 | if opts.report: 99 | errorlog.write_report() 100 | if errorlog.has_errors: 101 | print("WARNING: Errors encountered.", file=sys.stderr) 102 | sys.exit(-1) 103 | 104 | 105 | def main(): 106 | target_profiles = ["githubwiki", "stdwiki"] 107 | 108 | parser = argparse.ArgumentParser(prog='openscad-docsgen') 109 | parser.add_argument('-D', '--docs-dir', default="docs", 110 | help='The directory to put generated documentation in.') 111 | parser.add_argument('-T', '--test-only', action="store_true", 112 | help="If given, don't generate images, but do try executing the scripts.") 113 | parser.add_argument('-q', '--quiet', action="store_true", 114 | help="Suppress printing of progress data.") 115 | parser.add_argument('-S', '--strict', action="store_true", 116 | help="If given, require File/LibFile and Section headers.") 117 | parser.add_argument('-f', '--force', action="store_true", 118 | help='If given, force regeneration of images.') 119 | parser.add_argument('-n', '--no-images', action="store_true", 120 | help='If given, skips image generation.') 121 | parser.add_argument('-m', '--gen-files', action="store_true", 122 | help='If given, generate documents for each source file.') 123 | parser.add_argument('-i', '--gen-index', action="store_true", 124 | help='If given, generate AlphaIndex.md file.') 125 | parser.add_argument('-I', '--gen-topics', action="store_true", 126 | help='If given, generate Topics.md topics index file.') 127 | parser.add_argument('-t', '--gen-toc', action="store_true", 128 | help='If given, generate TOC.md table of contents file.') 129 | parser.add_argument('-g', '--gen-glossary', action="store_true", 130 | help='If given, generate Glossary.md file.') 131 | parser.add_argument('-c', '--gen-cheat', action="store_true", 132 | help='If given, generate CheatSheet.md file with all Usage lines.') 133 | parser.add_argument('-s', '--gen_sidebar', action="store_true", 134 | help="If given, generate _Sidebar.md file index.") 135 | parser.add_argument('-a', '--png-animation', action="store_true", 136 | help='If given, animations are created using animated PNGs instead of GIFs.') 137 | parser.add_argument('-P', '--project-name', 138 | help='If given, sets the name of the project to be shown in titles.') 139 | parser.add_argument('-r', '--report', action="store_true", 140 | help='If given, write all warnings and errors to docsgen_report.json') 141 | parser.add_argument('-d', '--dump-tree', action="store_true", 142 | help='If given, dumps the documentation tree for debugging.') 143 | parser.add_argument('-p', '--target-profile', choices=target_classes.keys(), default=default_target, 144 | help='Sets the output target profile. Defaults to "{}"'.format(default_target)) 145 | parser.add_argument('srcfiles', nargs='*', help='List of input source files.') 146 | opts = Options(parser.parse_args()) 147 | 148 | try: 149 | processFiles(opts) 150 | except DocsGenException as e: 151 | print(e) 152 | sys.exit(-1) 153 | except OSError as e: 154 | print(e) 155 | sys.exit(-1) 156 | except KeyboardInterrupt as e: 157 | print(" Aborting.", file=sys.stderr) 158 | sys.exit(-1) 159 | 160 | sys.exit(0) 161 | 162 | 163 | if __name__ == "__main__": 164 | main() 165 | 166 | 167 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 168 | -------------------------------------------------------------------------------- /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 | 12 | 13 | class DocsGenException(Exception): 14 | def __init__(self, block="", message=""): 15 | self.block = block 16 | self.message = message 17 | super().__init__('{} "{}"'.format(self.message, self.block)) 18 | 19 | 20 | class GenericBlock(object): 21 | _link_pat = re.compile(r'^(.*?)\{\{([A-Za-z0-9_()]+)\}\}(.*)$') 22 | 23 | def __init__(self, title, subtitle, body, origin, parent=None): 24 | self.title = title 25 | self.subtitle = subtitle 26 | self.body = body 27 | self.origin = origin 28 | self.parent = parent 29 | self.children = [] 30 | self.figure_num = 0 31 | self.definitions = {} 32 | if parent: 33 | parent.children.append(self) 34 | 35 | def __str__(self): 36 | return "{}: {}".format( 37 | self.title.replace('&', '/'), 38 | self.subtitle 39 | ) 40 | 41 | def get_figure_num(self): 42 | if self.parent: 43 | return self.parent.get_figure_num() 44 | return "" 45 | 46 | def sort_children(self, front_blocks=(), back_blocks=()): 47 | children = [] 48 | for blocks in front_blocks: 49 | for block in blocks: 50 | for child in self.children: 51 | if child.title.startswith(block): 52 | children.append(child) 53 | blocks = flatten(front_blocks + back_blocks) 54 | for child in self.children: 55 | found = [block for block in blocks if child.title.startswith(block)] 56 | if not found: 57 | children.append(child) 58 | for blocks in back_blocks: 59 | for block in blocks: 60 | for child in self.children: 61 | if child.title.startswith(block): 62 | children.append(child) 63 | return children 64 | 65 | def get_children_by_title(self, titles): 66 | if isinstance(titles,str): 67 | titles = [titles] 68 | return [ 69 | child 70 | for child in self.children 71 | if child.title in titles 72 | ] 73 | 74 | def get_data(self): 75 | d = { 76 | "name": self.title, 77 | "subtitle": self.subtitle, 78 | "body": self.body, 79 | "file": self.origin.file, 80 | "line": self.origin.line 81 | } 82 | if self.children: 83 | d["children"] = [child.get_data() for child in self.children] 84 | return d 85 | 86 | def get_link(self, target, currfile=None, literalize=False, html=False): 87 | return self.title 88 | 89 | def parse_links(self, line, controller, target, html=False): 90 | oline = "" 91 | while line: 92 | m = self._link_pat.match(line) 93 | if m: 94 | oline += m.group(1) 95 | name = m.group(2).lower().strip() 96 | line = m.group(3) 97 | literalize = name.endswith("()") 98 | if name in controller.items_by_name: 99 | item = controller.items_by_name[name] 100 | oline += item.get_link(target, currfile=self.origin.file, literalize=literalize, html=html) 101 | elif name in controller.definitions: 102 | oline += target.get_link(name, anchor=name.lower(), file="Glossary", literalize=literalize, html=html) 103 | elif name in controller.defn_aliases: 104 | term = controller.defn_aliases[name] 105 | oline += target.get_link(name, anchor=term.lower(), file="Glossary", literalize=literalize, html=html) 106 | else: 107 | print(controller.definitions) 108 | msg = "Invalid Link {{{{{0}}}}}".format(name) 109 | errorlog.add_entry(self.origin.file, self.origin.line, msg, ErrorLog.FAIL) 110 | oline += name 111 | else: 112 | oline += line 113 | line = "" 114 | return oline 115 | 116 | def get_markdown_body(self, controller, target): 117 | out = [] 118 | if not self.body: 119 | return out 120 | in_block = False 121 | for line in self.body: 122 | if line.startswith("```"): 123 | in_block = not in_block 124 | if in_block or line.startswith(" "): 125 | out.append(line) 126 | elif line == ".": 127 | out.append("") 128 | else: 129 | out.append(self.parse_links(line, controller, target)) 130 | return out 131 | 132 | def get_tocfile_lines(self, controller, target, n=1, currfile=""): 133 | return [] 134 | 135 | def get_toc_lines(self, controller, target, n=1, currfile=""): 136 | return [] 137 | 138 | def get_cheatsheet_lines(self, controller, target): 139 | return [] 140 | 141 | def get_file_lines(self, controller, target): 142 | sub = self.parse_links(self.subtitle, controller, target) 143 | out = target.block_header(self.title, sub) 144 | out.extend(self.get_markdown_body(controller, target)) 145 | out.append("") 146 | return out 147 | 148 | def __eq__(self, other): 149 | return self.title == other.title and self.subtitle == other.subtitle 150 | 151 | def __lt__(self, other): 152 | if self.subtitle == other.subtitle: 153 | return self.title < other.title 154 | return self.subtitle < other.subtitle 155 | 156 | 157 | class LabelBlock(GenericBlock): 158 | def __init__(self, title, subtitle, body, origin, parent=None): 159 | if body: 160 | raise DocsGenException(title, "Body not supported, while declaring block:") 161 | super().__init__(title, subtitle, body, origin, parent=parent) 162 | 163 | 164 | class SynopsisBlock(LabelBlock): 165 | def __init__(self, title, subtitle, body, origin, parent=None): 166 | parent.synopsis = subtitle 167 | super().__init__(title, subtitle, body, origin, parent=parent) 168 | 169 | def get_file_lines(self, controller, target): 170 | sub = self.parse_links(self.subtitle, controller, target) 171 | sub = target.escape_entities(sub) + target.mouseover_tags(self.parent.syntags, htag="sup", wrap="[{}]") 172 | out = target.block_header(self.title, sub, escsub=False) 173 | return out 174 | 175 | 176 | class SynTagsBlock(LabelBlock): 177 | def __init__(self, title, subtitle, body, origin, parent, syntags_data={}): 178 | tags = [x.strip() for x in subtitle.split(",")] 179 | for tag in tags: 180 | parent.syntags[tag] = syntags_data[tag] 181 | super().__init__(title, subtitle, body, origin, parent=parent) 182 | 183 | def get_file_lines(self, controller, target): 184 | return [] 185 | 186 | 187 | class TopicsBlock(LabelBlock): 188 | def __init__(self, title, subtitle, body, origin, parent=None): 189 | super().__init__(title, subtitle, body, origin, parent=parent) 190 | self.topics = [x.strip() for x in subtitle.split(",")] 191 | parent.topics = self.topics 192 | 193 | def get_file_lines(self, controller, target): 194 | links = [ 195 | target.get_link(topic, anchor=target.header_link(topic), file="Topics", literalize=False) 196 | for topic in self.topics 197 | ] 198 | links = ", ".join(links) 199 | out = target.block_header(self.title, links) 200 | return out 201 | 202 | 203 | class SeeAlsoBlock(LabelBlock): 204 | def __init__(self, title, subtitle, body, origin, parent=None): 205 | self.see_also = [x.strip() for x in subtitle.split(",")] 206 | parent.see_also = self.see_also 207 | super().__init__(title, subtitle, body, origin, parent=parent) 208 | 209 | def get_file_lines(self, controller, target): 210 | items = [] 211 | for name in self.see_also: 212 | if name not in controller.items_by_name: 213 | msg = "Invalid Link '{0}'".format(name) 214 | errorlog.add_entry(self.origin.file, self.origin.line, msg, ErrorLog.FAIL) 215 | else: 216 | item = controller.items_by_name[name] 217 | if item is not self.parent: 218 | items.append( item ) 219 | links = [ 220 | item.get_link(target, currfile=self.origin.file, literalize=False) 221 | for item in items 222 | ] 223 | out = [] 224 | links = ", ".join(links) 225 | out.extend(target.block_header(self.title, links, escsub=False)) 226 | return out 227 | 228 | 229 | class HeaderlessBlock(GenericBlock): 230 | def __init__(self, title, subtitle, body, origin, parent=None): 231 | if subtitle: 232 | body.insert(0, subtitle) 233 | subtitle = "" 234 | super().__init__(title, subtitle, body, origin, parent=parent) 235 | 236 | def get_file_lines(self, controller, target): 237 | out = [] 238 | out.append("") 239 | out.extend(self.get_markdown_body(controller, target)) 240 | out.append("") 241 | return out 242 | 243 | 244 | class TextBlock(GenericBlock): 245 | def __init__(self, title, subtitle, body, origin, parent=None): 246 | if subtitle: 247 | body.insert(0, subtitle) 248 | subtitle = "" 249 | super().__init__(title, subtitle, body, origin, parent=parent) 250 | 251 | 252 | class DefinitionsBlock(GenericBlock): 253 | def __init__(self, title, subtitle, body, origin, parent=None): 254 | super().__init__(title, subtitle, body, origin, parent=parent) 255 | terms = [] 256 | self.definitions = {} 257 | for line in body: 258 | if '=' not in line: 259 | raise DocsGenException(title, "Expected body line in the format TERM = DEFINITION, while declaring block:") 260 | termset, defn = line.split('=', 1) 261 | termset = [x.strip() for x in termset.lower().split('|')] 262 | defn = defn.strip() 263 | for term in termset: 264 | if term in terms: 265 | raise DocsGenException(title, "Redefined term '{}', while declaring block:".format(term)) 266 | terms.append(term) 267 | term = termset[0] 268 | self.definitions[term] = (termset, defn) 269 | 270 | def get_file_lines(self, controller, target): 271 | out = target.block_header(self.title, self.subtitle) 272 | terms = list(self.definitions.keys()) 273 | defs = { 274 | key: self.parse_links(info[1], controller, target, html=True) 275 | for key, info in self.definitions.items() 276 | } 277 | for term in terms: 278 | out.extend(target.markdown_block(["{}: {}".format(term.title(), defs[term])])) 279 | out.append("") 280 | return out 281 | 282 | 283 | class BulletListBlock(GenericBlock): 284 | def __init__(self, title, subtitle, body, origin, parent=None): 285 | super().__init__(title, subtitle, body, origin, parent=parent) 286 | 287 | def get_file_lines(self, controller, target): 288 | sub = self.parse_links(self.subtitle, controller, target) 289 | sub = target.escape_entities(sub) 290 | out = target.block_header(self.title, sub) 291 | out.extend(target.bullet_list(self.body)) 292 | return out 293 | 294 | 295 | class NumberedListBlock(GenericBlock): 296 | def __init__(self, title, subtitle, body, origin, parent=None): 297 | super().__init__(title, subtitle, body, origin, parent=parent) 298 | 299 | def get_file_lines(self, controller, target): 300 | sub = self.parse_links(self.subtitle, controller, target) 301 | sub = target.escape_entities(sub) 302 | out = target.block_header(self.title, sub) 303 | out.extend(target.numbered_list(self.body)) 304 | return out 305 | 306 | 307 | class TableBlock(GenericBlock): 308 | def __init__(self, title, subtitle, body, origin, parent=None, header_sets=None): 309 | super().__init__(title, subtitle, body, origin, parent=parent) 310 | self.header_sets = header_sets 311 | tnum = 0 312 | for line in self.body: 313 | if line == "---": 314 | tnum += 1 315 | continue 316 | if tnum >= len(self.header_sets): 317 | raise DocsGenException(title, "More tables than header_sets, while declaring block:") 318 | 319 | def get_file_lines(self, controller, target): 320 | tnum = 0 321 | table = [] 322 | tables = [] 323 | for line in self.body: 324 | if line == "---": 325 | tnum += 1 326 | if table: 327 | tables.append(table) 328 | table = [] 329 | continue 330 | hdr_set = self.header_sets[tnum] 331 | cells = [ 332 | self.parse_links(x.strip(), controller, target) 333 | for x in line.split("=",len(hdr_set)-1) 334 | ] 335 | table.append(cells) 336 | if table: 337 | tables.append(table) 338 | sub = self.parse_links(self.subtitle, controller, target) 339 | sub = target.escape_entities(sub) 340 | out = target.block_header(self.title, sub) 341 | for tnum, table in enumerate(tables): 342 | headers = self.header_sets[tnum] 343 | out.extend(target.table(headers,table)) 344 | return out 345 | 346 | 347 | class FileBlock(GenericBlock): 348 | def __init__(self, title, subtitle, body, origin): 349 | super().__init__(title, subtitle, body, origin) 350 | self.includes = [] 351 | self.common_code = [] 352 | self.footnotes = [] 353 | self.summary = "" 354 | self.group = "" 355 | 356 | def get_data(self): 357 | d = super().get_data() 358 | d["includes"] = self.includes 359 | d["commoncode"] = self.common_code 360 | d["group"] = self.group 361 | d["summary"] = self.summary 362 | d["footnotes"] = [ 363 | { 364 | "mark": mark, 365 | "note": note 366 | } for mark, note in self.footnotes 367 | ] 368 | skip_titles = ["CommonCode", "Includes"] 369 | d["children"] = list(filter(lambda x: x["name"] not in skip_titles, d["children"])) 370 | return d 371 | 372 | def get_link(self, target, currfile=None, label="", literalize=False, html=False): 373 | file = self.origin.file 374 | if currfile is None or self.origin.file == currfile: 375 | file = "" 376 | return target.get_link( 377 | label=label if label else str(self), 378 | anchor="", 379 | file=file, 380 | literalize=literalize, 381 | html=html 382 | ) 383 | 384 | def get_tocfile_lines(self, controller, target, n=1, currfile=""): 385 | sections = [ 386 | sect for sect in self.children 387 | if isinstance(sect, SectionBlock) 388 | ] 389 | link = self.get_link(target, label=self.subtitle, currfile=currfile) 390 | out = [] 391 | out.extend(target.header("{}. {}".format(n, link), lev=target.SECTION, esc=False)) 392 | if self.summary: 393 | out.extend(target.line_with_break(self.summary)) 394 | if self.footnotes: 395 | for mark, note, origin in self.footnotes: 396 | out.extend(target.line_with_break(target.italics(note))) 397 | out.extend(target.bullet_list_start()) 398 | for n, sect in enumerate(sections): 399 | out.extend(sect.get_tocfile_lines(controller, target, n=n+1, currfile=currfile)) 400 | out.extend(target.bullet_list_end()) 401 | return out 402 | 403 | def get_toc_lines(self, controller, target, n=1, currfile=""): 404 | sections = [ 405 | sect for sect in self.children 406 | if isinstance(sect, SectionBlock) 407 | ] 408 | out = [] 409 | out.extend(target.numbered_list_start()) 410 | for n, sect in enumerate(sections): 411 | out.extend(sect.get_toc_lines(controller, target, n=n+1, currfile=currfile)) 412 | out.extend(target.numbered_list_end()) 413 | return out 414 | 415 | def get_cheatsheet_lines(self, controller, target): 416 | lines = [] 417 | for child in self.get_children_by_title("Section"): 418 | lines.extend(child.get_cheatsheet_lines(controller, target)) 419 | out = [] 420 | if lines: 421 | out.extend(target.header("{}: {}".format(self.title, self.subtitle), lev=target.SUBSECTION)) 422 | out.extend(lines) 423 | return out 424 | 425 | def get_file_lines(self, controller, target): 426 | out = target.header(str(self), lev=target.FILE) 427 | out.extend(target.markdown_block(self.get_markdown_body(controller, target))) 428 | for child in self.children: 429 | if not isinstance(child, SectionBlock): 430 | out.extend(child.get_file_lines(controller, target)) 431 | out.extend(target.header("File Contents", lev=target.SECTION)) 432 | out.extend(self.get_toc_lines(controller, target, currfile=self.origin.file)) 433 | for child in self.children: 434 | if isinstance(child, SectionBlock): 435 | out.extend(child.get_file_lines(controller, target)) 436 | return out 437 | 438 | def get_figure_num(self): 439 | return "{}".format(self.figure_num) 440 | 441 | 442 | class IncludesBlock(GenericBlock): 443 | def __init__(self, title, subtitle, body, origin, parent=None): 444 | super().__init__(title, subtitle, body, origin, parent=parent) 445 | if parent: 446 | parent.includes.extend(body) 447 | 448 | def get_file_lines(self, controller, target): 449 | out = [] 450 | if self.body: 451 | out.extend(target.markdown_block([ 452 | "To use, add the following lines to the beginning of your file:" 453 | ])) 454 | out.extend(target.markdown_block(target.indent_lines(self.body))) 455 | return out 456 | 457 | 458 | class SectionBlock(GenericBlock): 459 | def __init__(self, title, subtitle, body, origin, parent=None): 460 | super().__init__(title, subtitle, body, origin, parent=parent) 461 | if parent: 462 | self.parent.figure_num += 1 463 | 464 | def get_link(self, target, currfile=None, label="", literalize=False, html=False): 465 | file = self.origin.file 466 | if currfile is None or self.origin.file == currfile: 467 | file = "" 468 | return target.get_link( 469 | label=label if label else str(self), 470 | anchor=target.header_link(str(self)), 471 | file=file, 472 | literalize=literalize, 473 | html=html 474 | ) 475 | 476 | def get_tocfile_lines(self, controller, target, n=1, currfile=""): 477 | """ 478 | Return the markdown table of contents lines for the children in this 479 | section. This is returned as a series of bullet points. 480 | `indent` sets the level of indentation for the bullet points 481 | """ 482 | out = [] 483 | if self.subtitle: 484 | item = self.get_link(target, label=self.subtitle, currfile=currfile) 485 | out.extend(target.line_with_break(target.bullet_list_item(item))) 486 | subsects = self.get_children_by_title("Subsection") 487 | if subsects: 488 | out.extend(target.bullet_list_start()) 489 | for child in subsects: 490 | out.extend(target.indent_lines(child.get_tocfile_lines(controller, target, currfile=currfile))) 491 | out.extend(target.bullet_list_end()) 492 | out.extend( 493 | target.indent_lines( 494 | target.bullet_list( 495 | flatten([ 496 | child.get_tocfile_lines(controller, target, currfile=currfile) 497 | for child in self.get_children_by_title( 498 | ["Constant","Function","Module","Function&Module"] 499 | ) 500 | ]) 501 | ) 502 | ) 503 | ) 504 | else: 505 | for child in self.get_children_by_title("Subsection"): 506 | out.extend(child.get_tocfile_lines(controller, target, currfile=currfile)) 507 | out.extend( 508 | target.indent_lines( 509 | target.bullet_list( 510 | flatten([ 511 | child.get_tocfile_lines(controller, target, currfile=currfile) 512 | for child in self.get_children_by_title( 513 | ["Constant","Function","Module","Function&Module"] 514 | ) 515 | ]) 516 | ) 517 | ) 518 | ) 519 | return out 520 | 521 | def get_toc_lines(self, controller, target, n=1, currfile=""): 522 | """ 523 | Return the markdown table of contents lines for the children in this 524 | section. This is returned as a series of bullet points. 525 | `indent` sets the level of indentation for the bullet points 526 | """ 527 | lines = [] 528 | subsects = self.get_children_by_title("Subsection") 529 | if subsects: 530 | lines.extend(target.numbered_list_start()) 531 | for num, child in enumerate(subsects): 532 | lines.extend(child.get_toc_lines(controller, target, currfile=currfile, n=num+1)) 533 | lines.extend(target.numbered_list_end()) 534 | for child in self.get_children_by_title(["Constant","Function","Module","Function&Module"]): 535 | lines.extend(child.get_toc_lines(controller, target, currfile=currfile)) 536 | out = [] 537 | if self.subtitle: 538 | item = self.get_link(target, currfile=currfile) 539 | out.extend(target.numbered_list_item(n, item)) 540 | out.extend(target.bullet_list_start()) 541 | out.extend(target.indent_lines(lines)) 542 | out.extend(target.bullet_list_end()) 543 | else: 544 | out.extend(target.bullet_list_start()) 545 | out.extend(lines) 546 | out.extend(target.bullet_list_end()) 547 | return out 548 | 549 | def get_cheatsheet_lines(self, controller, target): 550 | subs = [] 551 | for child in self.get_children_by_title("Subsection"): 552 | subs.extend(child.get_cheatsheet_lines(controller, target)) 553 | consts = [] 554 | for cnst in self.get_children_by_title("Constant"): 555 | consts.append(cnst.get_link(target, currfile="CheatSheet")) 556 | for alias in cnst.aliases: 557 | consts.append(cnst.get_link(target, label=alias, currfile="CheatSheet")) 558 | items = [] 559 | for child in self.get_children_by_title(["Function","Module","Function&Module"]): 560 | items.extend(child.get_cheatsheet_lines(controller, target)) 561 | out = [] 562 | if subs or consts or items: 563 | out.extend(target.header("{}: {}".format(self.title, self.subtitle), lev=target.ITEM)) 564 | out.extend(subs) 565 | if consts: 566 | out.append("Constants: " + " ".join(consts)) 567 | out.extend(items) 568 | out.append("") 569 | return out 570 | 571 | def get_file_lines(self, controller, target): 572 | """ 573 | Return the markdown for this section. This includes the section 574 | heading and the markdown for the children. 575 | """ 576 | out = [] 577 | if self.subtitle: 578 | out.extend(target.header(str(self), lev=target.SECTION)) 579 | out.extend(target.markdown_block(self.get_markdown_body(controller, target))) 580 | for child in self.children: 581 | out.extend(child.get_file_lines(controller, target)) 582 | return out 583 | 584 | def get_figure_num(self): 585 | hdr = (self.parent.get_figure_num() + ".") if self.parent else "" 586 | return "{}{}".format(hdr, self.figure_num) 587 | 588 | 589 | class SubsectionBlock(GenericBlock): 590 | def __init__(self, title, subtitle, body, origin, parent=None): 591 | super().__init__(title, subtitle, body, origin, parent=parent) 592 | if parent: 593 | self.parent.figure_num += 1 594 | 595 | def get_link(self, target, currfile=None, label="", literalize=False, html=False): 596 | file = self.origin.file 597 | if currfile is None or self.origin.file == currfile: 598 | file = "" 599 | return target.get_link( 600 | label=label if label else str(self), 601 | anchor=target.header_link(str(self)), 602 | file=file, 603 | literalize=literalize, 604 | html=html 605 | ) 606 | 607 | def get_tocfile_lines(self, controller, target, n=1, currfile=""): 608 | """ 609 | Return the markdown table of contents lines for the children in this 610 | subsection. This is returned as a series of bullet points. 611 | `indent` sets the level of indentation for the bullet points 612 | """ 613 | out = [] 614 | item = self.get_link(target, label=self.subtitle, currfile=currfile) 615 | out.extend(target.bullet_list_item(item)) 616 | items = self.get_children_by_title(["Constant","Function","Module","Function&Module"]) 617 | if items: 618 | out.extend( 619 | target.indent_lines( 620 | target.bullet_list( 621 | flatten([ 622 | child.get_tocfile_lines(controller, target, currfile=currfile) 623 | for child in items 624 | ]) 625 | ) 626 | ) 627 | ) 628 | return out 629 | 630 | def get_toc_lines(self, controller, target, n=1, currfile=""): 631 | """ 632 | Return the markdown table of contents lines for the children in this 633 | subsection. This is returned as a series of bullet points. 634 | `indent` sets the level of indentation for the bullet points 635 | """ 636 | lines = [] 637 | for child in self.get_children_by_title(["Constant","Function","Module","Function&Module"]): 638 | lines.extend(child.get_toc_lines(controller, target, currfile=currfile)) 639 | out = [] 640 | if self.subtitle: 641 | item = self.get_link(target, currfile=currfile) 642 | out.extend(target.numbered_list_item(n, item)) 643 | if lines: 644 | out.extend(target.bullet_list_start()) 645 | out.extend(target.indent_lines(lines)) 646 | out.extend(target.bullet_list_end()) 647 | elif lines: 648 | out.extend(target.bullet_list_start()) 649 | out.extend(lines) 650 | out.extend(target.bullet_list_end()) 651 | return out 652 | 653 | def get_cheatsheet_lines(self, controller, target): 654 | consts = [] 655 | for cnst in self.get_children_by_title("Constant"): 656 | consts.append(cnst.get_link(target, currfile="CheatSheet")) 657 | for alias in cnst.aliases: 658 | consts.append(cnst.get_link(target, label=alias, currfile="CheatSheet")) 659 | items = [] 660 | for child in self.get_children_by_title(["Function","Module","Function&Module"]): 661 | items.extend(child.get_cheatsheet_lines(controller, target)) 662 | out = [] 663 | if consts or items: 664 | out.extend(target.header("{}: {}".format(self.title, self.subtitle), lev=target.ITEM)) 665 | if consts: 666 | out.append("Constants: " + " ".join(consts)) 667 | out.extend(items) 668 | out.append("") 669 | return out 670 | 671 | def get_file_lines(self, controller, target): 672 | """ 673 | Return the markdown for this section. This includes the section 674 | heading and the markdown for the children. 675 | """ 676 | out = [] 677 | if self.subtitle: 678 | out.extend(target.header(str(self), lev=target.SUBSECTION)) 679 | out.extend(target.markdown_block(self.get_markdown_body(controller, target))) 680 | for child in self.children: 681 | out.extend(child.get_file_lines(controller, target)) 682 | return out 683 | 684 | def get_figure_num(self): 685 | hdr = (self.parent.get_figure_num() + ".") if self.parent else "" 686 | return "{}{}".format(hdr, self.figure_num) 687 | 688 | 689 | class ItemBlock(LabelBlock): 690 | _paren_pat = re.compile(r'\([^\)]+\)') 691 | 692 | def __init__(self, title, subtitle, body, origin, parent=None): 693 | if self._paren_pat.search(subtitle): 694 | raise DocsGenException(title, "Text between parentheses, while declaring block:") 695 | super().__init__(title, subtitle, body, origin, parent=parent) 696 | self.example_num = 0 697 | self.deprecated = False 698 | self.topics = [] 699 | self.aliases = [] 700 | self.see_also = [] 701 | self.synopsis = "" 702 | self.syntags = {} 703 | if parent: 704 | self.parent.figure_num += 1 705 | 706 | def __str__(self): 707 | return "{}: {}".format( 708 | self.title.replace('&', '/'), 709 | re.sub(r'\([^\)]*\)', r'()', self.subtitle) 710 | ) 711 | 712 | def get_link(self, target, currfile=None, label="", literalize=True, html=False): 713 | file = self.origin.file 714 | if currfile is None or self.origin.file == currfile: 715 | file = "" 716 | return target.get_link( 717 | label=label if label else self.subtitle, 718 | anchor=target.header_link( 719 | "{}: {}".format(self.title, self.subtitle) 720 | ), 721 | file=file, 722 | literalize=literalize, 723 | html=html 724 | ) 725 | 726 | def get_data(self): 727 | d = super().get_data() 728 | if self.deprecated: 729 | d["deprecated"] = True 730 | d["topics"] = self.topics 731 | d["aliases"] = self.aliases 732 | d["synopsis"] = self.synopsis 733 | d["syntags"] = self.syntags 734 | d["see_also"] = self.see_also 735 | d["description"] = [ 736 | line 737 | for item in self.get_children_by_title("Description") 738 | for line in item.body 739 | ] 740 | d["arguments"] = [ 741 | line 742 | for item in self.get_children_by_title("Arguments") 743 | for line in item.body 744 | ] 745 | d["usages"] = [ 746 | { 747 | "subtitle": item.subtitle, 748 | "body": item.body 749 | } 750 | for item in self.get_children_by_title("Usage") 751 | ] 752 | d["examples"] = [ 753 | item.body 754 | for item in self.children if item.title.startswith("Example") 755 | ] 756 | skip_titles = ["Alias", "Aliases", "Arguments", "Description", "See Also", "Synopsis", "SynTags", "Status", "Topics", "Usage"] 757 | d["children"] = list(filter(lambda x: x["name"] not in skip_titles and not x["name"].startswith("Example"), d["children"])) 758 | return d 759 | 760 | def get_synopsis(self, controller, target): 761 | sub = self.parse_links(self.synopsis, controller, target) 762 | out = "{}{}{}".format( 763 | " – " if self.synopsis or self.syntags else "", 764 | target.escape_entities(sub), 765 | target.mouseover_tags(self.syntags, htag="sup", wrap="[{}]"), 766 | ) 767 | return out 768 | 769 | def get_funmod(self): 770 | funmod = self.title 771 | if funmod == "Function": 772 | funmod = "Func" 773 | elif funmod == "Module": 774 | funmod = "Mod" 775 | elif funmod == "Function&Module": 776 | funmod = "Func/Mod" 777 | elif funmod == "Constant": 778 | funmod = "Const" 779 | return funmod 780 | 781 | def get_index_line(self, controller, target, file): 782 | out = "{} {}{}".format( 783 | self.get_link(target, currfile=file), 784 | self.get_funmod(), 785 | self.get_synopsis(controller, target), 786 | ) 787 | return out 788 | 789 | def get_tocfile_lines(self, controller, target, n=1, currfile=""): 790 | out = [ 791 | self.get_index_line(controller, target, currfile) 792 | ] 793 | return out 794 | 795 | def get_toc_lines(self, controller, target, n=1, currfile=""): 796 | out = target.bullet_list_item( 797 | "{}{}".format( 798 | self.get_link(target, currfile=currfile), 799 | self.get_synopsis(controller, target), 800 | ) 801 | ) 802 | return out 803 | 804 | def get_cheatsheet_lines(self, controller, target): 805 | oline = "" 806 | item_name = re.sub(r'[^A-Za-z0-9_$]', r'', self.subtitle) 807 | link = self.get_link(target, currfile="CheatSheet", label=item_name, literalize=False) 808 | parts = [] 809 | part_lens = [] 810 | for usage in self.get_children_by_title("Usage"): 811 | for line in usage.body: 812 | part_lens.append(len(line)) 813 | line = target.escape_entities(line).replace( 814 | target.escape_entities(item_name), 815 | link 816 | ) 817 | parts.append(line) 818 | out = [] 819 | line = "" 820 | line_len = 0 821 | for part, part_len in zip(parts, part_lens): 822 | part = target.code_span(part) 823 | part = target.line_with_break(part)[0] 824 | if line_len + part_len > 80 or part_len > 40: 825 | if line: 826 | line = target.quote(line)[0]; 827 | out.append(line) 828 | line = part 829 | line_len = part_len 830 | else: 831 | if line: 832 | line += "    " 833 | line += part 834 | line_len += part_len 835 | if line: 836 | line = target.quote(line)[0]; 837 | out.extend(target.paragraph([line])) 838 | return out 839 | 840 | def get_file_lines(self, controller, target): 841 | front_blocks = [ 842 | ["Status"], 843 | ["Alias"], 844 | ["Synopsis"], 845 | ["Topics"], 846 | ["See Also"], 847 | ["Usage"] 848 | ] 849 | back_blocks = [ 850 | ["Example"] 851 | ] 852 | children = self.sort_children(front_blocks, back_blocks) 853 | out = [] 854 | out.extend(target.header(str(self), lev=target.ITEM)) 855 | for child in children: 856 | out.extend(child.get_file_lines(controller, target)) 857 | out.extend(target.horizontal_rule()) 858 | return out 859 | 860 | def get_figure_num(self): 861 | hdr = (self.parent.get_figure_num() + ".") if self.parent else "" 862 | return "{}{}".format(hdr, self.figure_num) 863 | 864 | 865 | class ImageBlock(GenericBlock): 866 | def __init__(self, title, subtitle, body, origin, parent=None, meta="", use_apngs=False): 867 | super().__init__(title, subtitle, body, origin, parent=parent) 868 | fileblock = parent 869 | while fileblock.parent: 870 | fileblock = fileblock.parent 871 | 872 | self.meta = meta 873 | self.image_url = None 874 | self.image_url_rel = None 875 | self.image_req = None 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 | 887 | san_name = re.sub(r'[^A-Za-z0-9_-]', r'', os.path.basename(parent.subtitle.strip().lower().replace(" ","-"))) 888 | if use_apngs: 889 | file_ext = "png" 890 | elif "Spin" in self.meta or "Anim" in self.meta: 891 | file_ext = "gif" 892 | else: 893 | file_ext = "png" 894 | if self.title == "Figure": 895 | parent.figure_num += 1 896 | fignum = self.get_figure_num() 897 | figsan = fignum.replace(".","_") 898 | proposed_name = "figure_{}.{}".format(figsan, file_ext) 899 | self.title = "{} {}".format(self.title, fignum) 900 | else: 901 | parent.example_num += 1 902 | image_num = parent.example_num 903 | img_suffix = "_{}".format(image_num) if image_num > 1 else "" 904 | proposed_name = "{}{}.{}".format(san_name, img_suffix, file_ext) 905 | self.title = "{} {}".format(self.title, image_num) 906 | 907 | file_dir, file_name = os.path.split(fileblock.origin.file.strip()) 908 | file_base = os.path.splitext(file_name)[0] 909 | self.image_url_rel = os.path.join("images", file_base, proposed_name) 910 | self.image_url = os.path.join(file_dir, self.image_url_rel) 911 | 912 | def generate_image(self, target): 913 | self.image_req = None 914 | if "NORENDER" in self.meta: 915 | return 916 | show_img = ( 917 | any(x in self.meta for x in ("2D", "3D", "Spin", "Anim")) or 918 | self.title.startswith("Figure") or 919 | self.parent.title in ("File", "LibFile", "Section", "Subsection", "Module", "Function&Module") 920 | ) 921 | if show_img: 922 | outfile = os.path.join(target.docs_dir, self.image_url) 923 | outdir = os.path.dirname(outfile) 924 | os.makedirs(outdir, mode=0o744, exist_ok=True) 925 | 926 | self.image_req = image_manager.new_request( 927 | self.origin.file, self.origin.line, 928 | outfile, self.raw_script, self.meta, 929 | starting_cb=self._img_proc_start, 930 | completion_cb=self._img_proc_done 931 | ) 932 | 933 | def get_data(self): 934 | d = super().get_data() 935 | d["script"] = self.raw_script 936 | d["imgurl"] = self.image_url 937 | return d 938 | 939 | def _img_proc_start(self, req): 940 | print(" {}... ".format(os.path.basename(self.image_url)), end='') 941 | sys.stdout.flush() 942 | 943 | def _img_proc_done(self, req): 944 | if req.success: 945 | if req.status == "SKIP": 946 | print() 947 | else: 948 | print(req.status) 949 | sys.stdout.flush() 950 | return 951 | pfx = " " 952 | out = "Failed OpenSCAD script:\n" 953 | out += pfx + "Image: {}\n".format( os.path.basename(req.image_file) ) 954 | out += pfx + "cmd-line = {}\n".format(" ".join(req.cmdline)) 955 | for line in req.stdout: 956 | out += pfx + line + "\n" 957 | for line in req.stderr: 958 | out += pfx + line + "\n" 959 | out += pfx + "Return code = {}\n".format(req.return_code) 960 | out += pfx + ("-=" * 32) + "-\n" 961 | for line in req.script_lines: 962 | out += pfx + line + "\n" 963 | out += pfx + ("=-" * 32) + "=" 964 | print("", file=sys.stderr) 965 | sys.stderr.flush() 966 | errorlog.add_entry(req.src_file, req.src_line, out, ErrorLog.FAIL) 967 | 968 | def get_file_lines(self, controller, target): 969 | fileblock = self.parent 970 | while fileblock.parent: 971 | fileblock = fileblock.parent 972 | out = [] 973 | if "Hide" in self.meta: 974 | return out 975 | 976 | self.generate_image(target) 977 | 978 | code = [] 979 | code.extend([line for line in fileblock.includes]) 980 | code.extend([line for line in self.body if not line.strip().startswith("--")]) 981 | 982 | do_render = "NORENDER" not in self.meta and ( 983 | self.parent.title in ["Module", "Function&Module"] or 984 | any(tag in self.meta for tag in ["2D","3D","Spin","Anim"]) 985 | ) 986 | 987 | code_below = False 988 | width = '' 989 | height = '' 990 | if self.image_req: 991 | code_below = self.image_req.script_under 992 | width = int(self.image_req.imgsize[0]) 993 | height = int(self.image_req.imgsize[1]) 994 | sub = self.parse_links(self.subtitle, controller, target) 995 | sub = target.escape_entities(sub) 996 | if "Figure" in self.title: 997 | 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)) 998 | elif not do_render: 999 | out.extend(target.image_block(self.parent.subtitle, self.title, sub, code=code, code_below=code_below, width=width, height=height)) 1000 | else: 1001 | 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)) 1002 | return out 1003 | 1004 | 1005 | class FigureBlock(ImageBlock): 1006 | def __init__(self, title, subtitle, body, origin, parent, meta="", use_apngs=False): 1007 | super().__init__(title, subtitle, body, origin, parent=parent, meta=meta, use_apngs=use_apngs) 1008 | 1009 | 1010 | class ExampleBlock(ImageBlock): 1011 | def __init__(self, title, subtitle, body, origin, parent, meta="", use_apngs=False): 1012 | super().__init__(title, subtitle, body, origin, parent=parent, meta=meta, use_apngs=use_apngs) 1013 | 1014 | 1015 | 1016 | 1017 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 1018 | -------------------------------------------------------------------------------- /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/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/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): 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.script_lines = [ 38 | line[2:] if line.startswith("--") else line 39 | for line in script_lines 40 | ] 41 | self.completion_cb = completion_cb 42 | self.starting_cb = starting_cb 43 | self.verbose = verbose 44 | 45 | self.render_mode = RenderMode.preview 46 | self.imgsize = (320, 240) 47 | self.camera = None 48 | self.animation_frames = None 49 | self.frame_ms = 250 50 | self.show_edges = "Edges" in image_meta 51 | self.show_axes = "NoAxes" not in image_meta 52 | self.show_scales = "NoScales" not in image_meta 53 | self.orthographic = "Perspective" not in image_meta 54 | self.script_under = False 55 | self.color_scheme = ColorScheme.cornfield.value 56 | 57 | if "ThrownTogether" in image_meta: 58 | self.render_mode = RenderMode.thrown_together 59 | elif "Render" in image_meta: 60 | self.render_mode = RenderMode.render 61 | 62 | m = self._size_re.search(image_meta) 63 | scale = 1.0 64 | if m: 65 | self.imgsize = (int(m.group(1)), int(m.group(2))) 66 | elif "Small" in image_meta: 67 | scale = 0.75 68 | elif "Med" in image_meta: 69 | scale = 1.5 70 | elif "Big" in image_meta: 71 | scale = 2.0 72 | elif "Huge" in image_meta: 73 | scale = 2.5 74 | self.imgsize = [scale*x for x in self.imgsize] 75 | 76 | vpt = [0, 0, 0] 77 | vpr = [55, 0, 25] 78 | vpd = 444 79 | vpf = 22.5 80 | dynamic_vp = False 81 | 82 | if "3D" in image_meta: 83 | vpr = [55, 0, 25] 84 | elif "2D" in image_meta: 85 | vpr = [0, 0, 0] 86 | if "FlatSpin" in image_meta: 87 | vpr = [55, 0, "360*$t"] 88 | dynamic_vp = True 89 | elif "Spin" in image_meta: 90 | vpr = ["90-45*cos(360*$t)", 0, "360*$t"] 91 | dynamic_vp = True 92 | elif "XSpin" in image_meta: 93 | vpr = ["360*$t", 0, 25] 94 | dynamic_vp = True 95 | elif "YSpin" in image_meta: 96 | vpr = [55, "360*$t", 25] 97 | dynamic_vp = True 98 | if "Anim" in image_meta: 99 | dynamic_vp = True 100 | 101 | match = self._vpr_re.search(image_meta) 102 | if match: 103 | vpr, dyn_vp = self._parse_vp_line(match.group(1), vpr, dynamic_vp) 104 | dynamic_vp = dynamic_vp or dyn_vp 105 | match = self._vpt_re.search(image_meta) 106 | if match: 107 | vpt, dyn_vp = self._parse_vp_line(match.group(1), vpt, dynamic_vp) 108 | dynamic_vp = dynamic_vp or dyn_vp 109 | match = self._vpd_re.search(image_meta) 110 | if match: 111 | vpd = float(match.group(1)) 112 | dynamic_vp = True 113 | match = self._vpf_re.search(image_meta) 114 | if match: 115 | vpf = float(match.group(1)) 116 | dynamic_vp = True 117 | 118 | if dynamic_vp: 119 | self.camera = None 120 | self.script_lines[0:0] = [ 121 | "$vpt = [{}, {}, {}];".format(*vpt), 122 | "$vpr = [{}, {}, {}];".format(*vpr), 123 | "$vpd = {};".format(vpd), 124 | "$vpf = {};".format(vpf), 125 | ] 126 | else: 127 | self.camera = [vpt[0],vpt[1],vpt[2], vpr[0],vpr[1],vpr[2], vpd] 128 | 129 | match = self._fps_re.search(image_meta) 130 | if match: 131 | self.frame_ms = int(1000/match.group(1)) 132 | match = self._framems_re.search(image_meta) 133 | if match: 134 | self.frame_ms = int(match.group(1)) 135 | 136 | if "Spin" in image_meta or "Anim" in image_meta: 137 | self.animation_frames = 36 138 | match = self._frames_re.search(image_meta) 139 | if match: 140 | self.animation_frames = int(match.group(1)) 141 | 142 | color_scheme_match = self._color_scheme_re.search(image_meta) 143 | if color_scheme_match: 144 | self.color_scheme = color_scheme_match.group(1) 145 | 146 | longest = max(len(line) for line in self.script_lines) 147 | maxlen = (880 - self.imgsize[0]) / 9 148 | if longest > maxlen or "ScriptUnder" in image_meta: 149 | self.script_under = True 150 | 151 | self.complete = False 152 | self.status = "INCOMPLETE" 153 | self.success = False 154 | self.cmdline = [] 155 | self.return_code = None 156 | self.stdout = [] 157 | self.stderr = [] 158 | self.echos = [] 159 | self.warnings = [] 160 | self.errors = [] 161 | 162 | def _parse_vp_line(self, line, old_trio, dynamic): 163 | comps = line.split(",") 164 | trio = [] 165 | if len(comps) == 3: 166 | for comp in comps: 167 | comp = comp.strip() 168 | try: 169 | trio.append(float(comp)) 170 | except ValueError: 171 | trio.append(comp) 172 | dynamic = True 173 | trio = trio if trio else old_trio 174 | return trio, dynamic 175 | 176 | def starting(self): 177 | if self.starting_cb: 178 | self.starting_cb(self) 179 | 180 | def completed(self, status, osc=None): 181 | self.complete = True 182 | self.status = status 183 | if osc: 184 | self.success = osc.success 185 | self.cmdline = osc.cmdline 186 | self.return_code = osc.return_code 187 | self.stdout = osc.stdout 188 | self.stderr = osc.stderr 189 | self.echos = osc.echos 190 | self.warnings = osc.warnings 191 | self.errors = osc.errors 192 | else: 193 | self.success = True 194 | if self.completion_cb: 195 | self.completion_cb(self) 196 | 197 | 198 | class ImageManager(object): 199 | 200 | def __init__(self): 201 | self.requests = [] 202 | self.test_only = False 203 | 204 | def purge_requests(self): 205 | self.requests = [] 206 | 207 | def new_request(self, src_file, src_line, image_file, script_lines, image_meta, starting_cb=None, completion_cb=None, verbose=False): 208 | if "NORENDER" in image_meta: 209 | raise Exception("Cannot render scripts marked NORENDER") 210 | req = ImageRequest(src_file, src_line, image_file, script_lines, image_meta, starting_cb, completion_cb, verbose=verbose) 211 | self.requests.append(req) 212 | return req 213 | 214 | def process_requests(self, test_only=False): 215 | self.test_only = test_only 216 | for req in self.requests: 217 | self.process_request(req) 218 | self.requests = [] 219 | 220 | def process_request(self, req): 221 | req.starting() 222 | 223 | dir_name = os.path.dirname(req.image_file) 224 | base_name = os.path.basename(req.image_file) 225 | file_base, file_ext = os.path.splitext(base_name) 226 | script_file = "tmp_{0}.scad".format(base_name.replace(".", "_")) 227 | targ_img_file = req.image_file 228 | new_img_file = "tmp_{0}{1}".format(file_base, file_ext) 229 | 230 | with open(script_file, "w") as f: 231 | for line in req.script_lines: 232 | f.write(line + "\n") 233 | 234 | try: 235 | no_vp = True 236 | for line in req.script_lines: 237 | if "$vp" in line: 238 | no_vp = False 239 | 240 | render_mode = req.render_mode 241 | animate = req.animation_frames 242 | if self.test_only: 243 | render_mode = RenderMode.test_only 244 | animate = None 245 | 246 | osc = OpenScadRunner( 247 | script_file, 248 | new_img_file, 249 | animate=animate, 250 | animate_duration=req.frame_ms, 251 | imgsize=req.imgsize, 252 | antialias=2, 253 | orthographic=True, 254 | camera=req.camera, 255 | auto_center=no_vp, 256 | view_all=no_vp, 257 | color_scheme = req.color_scheme, 258 | show_edges=req.show_edges, 259 | show_axes=req.show_axes, 260 | show_scales=req.show_scales, 261 | render_mode=render_mode, 262 | hard_warnings=no_vp, 263 | verbose=req.verbose 264 | ) 265 | osc.run() 266 | osc.warnings = [line for line in osc.warnings if "Viewall and autocenter disabled" not in line] 267 | 268 | finally: 269 | os.unlink(script_file) 270 | 271 | if not osc.good() or osc.warnings or osc.errors: 272 | osc.success = False 273 | req.completed("FAIL", osc) 274 | return 275 | 276 | if self.test_only: 277 | req.completed("SKIP", osc) 278 | return 279 | 280 | os.makedirs(os.path.dirname(targ_img_file), exist_ok=True) 281 | 282 | # Time to compare image. 283 | if not os.path.isfile(targ_img_file): 284 | os.rename(new_img_file, targ_img_file) 285 | req.completed("NEW", osc) 286 | elif self.image_compare(targ_img_file, new_img_file): 287 | os.unlink(new_img_file) 288 | req.completed("SKIP", osc) 289 | else: 290 | os.unlink(targ_img_file) 291 | os.rename(new_img_file, targ_img_file) 292 | req.completed("REPLACE", osc) 293 | 294 | @staticmethod 295 | def image_compare(file1, file2, max_diff=64.0): 296 | """ 297 | Compare two image files. Returns true if they are almost exactly the same. 298 | """ 299 | if file1.endswith(".gif") and file2.endswith(".gif"): 300 | return filecmp.cmp(file1, file2, shallow=False) 301 | else: 302 | img1 = imread(file1).astype(float) 303 | img2 = imread(file2).astype(float) 304 | # calculate the difference and its norms 305 | diff = img1 - img2 # elementwise for scipy arrays 306 | diff_max = numpy.max(abs(diff)) 307 | return diff_max <= max_diff 308 | 309 | 310 | image_manager = ImageManager() 311 | 312 | 313 | 314 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 315 | -------------------------------------------------------------------------------- /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 .filehashes import FileHashes 16 | 17 | 18 | class MarkdownImageGen(object): 19 | HASHFILE = ".source_hashes" 20 | 21 | def __init__(self, opts): 22 | self.opts = opts 23 | self.filehashes = FileHashes(os.path.join(self.opts.docs_dir, self.HASHFILE)) 24 | 25 | def img_started(self, req): 26 | print(" {}... ".format(os.path.basename(req.image_file)), end='') 27 | sys.stdout.flush() 28 | 29 | def img_completed(self, req): 30 | if req.success: 31 | if req.status == "SKIP": 32 | print() 33 | else: 34 | print(req.status) 35 | sys.stdout.flush() 36 | return 37 | out = "\n\n" 38 | for line in req.echos: 39 | out += line + "\n" 40 | for line in req.warnings: 41 | out += line + "\n" 42 | for line in req.errors: 43 | out += line + "\n" 44 | out += "//////////////////////////////////////////////////////////////////////\n" 45 | out += "// LibFile: {} Line: {} Image: {}\n".format( 46 | req.src_file, req.src_line, os.path.basename(req.image_file) 47 | ) 48 | out += "//////////////////////////////////////////////////////////////////////\n" 49 | for line in req.script_lines: 50 | out += line + "\n" 51 | out += "//////////////////////////////////////////////////////////////////////\n" 52 | errorlog.add_entry(req.src_file, req.src_line, out, ErrorLog.FAIL) 53 | sys.stderr.flush() 54 | 55 | def processFiles(self, srcfiles): 56 | opts = self.opts 57 | image_root = os.path.join(opts.docs_dir, opts.image_root) 58 | for infile in srcfiles: 59 | fileroot = os.path.splitext(os.path.basename(infile))[0] 60 | outfile = os.path.join(opts.docs_dir, opts.file_prefix + fileroot + ".md") 61 | print(outfile) 62 | sys.stdout.flush() 63 | 64 | out = [] 65 | with open(infile, "r") as f: 66 | script = [] 67 | extyp = "" 68 | in_script = False 69 | imgnum = 0 70 | show_script = True 71 | linenum = -1 72 | for line in f.readlines(): 73 | linenum += 1 74 | line = line.rstrip("\n") 75 | if line.startswith("```openscad"): 76 | in_script = True; 77 | if "-" in line: 78 | extyp = line.split("-")[1] 79 | else: 80 | extyp = "" 81 | show_script = "ImgOnly" not in extyp 82 | script = [] 83 | imgnum = imgnum + 1 84 | elif in_script: 85 | if line == "```": 86 | in_script = False 87 | if opts.png_animation: 88 | fext = "png" 89 | elif any(x in extyp for x in ("Anim", "Spin")): 90 | fext = "gif" 91 | else: 92 | fext = "png" 93 | fname = "{}_{}.{}".format(fileroot, imgnum, fext) 94 | img_rel_url = os.path.join(opts.image_root, fname) 95 | imgfile = os.path.join(opts.docs_dir, img_rel_url) 96 | image_manager.new_request( 97 | fileroot+".md", linenum, 98 | imgfile, script, extyp, 99 | starting_cb=self.img_started, 100 | completion_cb=self.img_completed 101 | ) 102 | if show_script: 103 | out.append("```openscad") 104 | for line in script: 105 | if not line.startswith("--"): 106 | out.append(line) 107 | out.append("```") 108 | out.append("![Figure {}]({})".format(imgnum, img_rel_url)) 109 | show_script = True 110 | extyp = "" 111 | else: 112 | script.append(line) 113 | else: 114 | out.append(line) 115 | 116 | if not opts.test_only: 117 | with open(outfile, "w") as f: 118 | for line in out: 119 | print(line, file=f) 120 | 121 | has_changed = self.filehashes.is_changed(infile) 122 | if opts.force or opts.test_only or has_changed: 123 | image_manager.process_requests(test_only=opts.test_only) 124 | image_manager.purge_requests() 125 | 126 | if errorlog.file_has_errors(infile): 127 | self.filehashes.invalidate(infile) 128 | self.filehashes.save() 129 | 130 | 131 | def mdimggen_main(): 132 | rcfile = ".openscad_mdimggen_rc" 133 | defaults = {} 134 | if os.path.exists(rcfile): 135 | with open(rcfile, "r") as f: 136 | data = yaml.safe_load(f) 137 | if data is not None: 138 | defaults = data 139 | 140 | parser = argparse.ArgumentParser(prog='openscad-mdimggen') 141 | parser.add_argument('-D', '--docs-dir', default=defaults.get("docs_dir", "docs"), 142 | help='The directory to put generated documentation in.') 143 | parser.add_argument('-P', '--file-prefix', default=defaults.get("file_prefix", ""), 144 | help='The prefix to put in front of each output markdown file.') 145 | parser.add_argument('-T', '--test-only', action="store_true", 146 | help="If given, don't generate images, but do try executing the scripts.") 147 | parser.add_argument('-I', '--image_root', default=defaults.get("image_root", "images"), 148 | help='The directory to put generated images in.') 149 | parser.add_argument('-f', '--force', action="store_true", 150 | help='If given, force regeneration of images.') 151 | parser.add_argument('-a', '--png-animation', action="store_true", 152 | default=defaults.get("png_animations", True), 153 | help='If given, animations are created using animated PNGs instead of GIFs.') 154 | parser.add_argument('srcfiles', nargs='*', help='List of input markdown files.') 155 | args = parser.parse_args() 156 | 157 | if not args.srcfiles: 158 | srcfiles = defaults.get("source_files", []) 159 | if isinstance(srcfiles, str): 160 | args.srcfiles = glob.glob(srcfiles) 161 | elif isinstance(srcfiles, list): 162 | args.srcfiles = [] 163 | for srcfile in srcfiles: 164 | if isinstance(srcfile, str): 165 | args.srcfiles.extend(glob.glob(srcfile)) 166 | elif platform.system() == 'Windows': 167 | args.srcfiles = [file for src_file in args.srcfiles for file in glob.glob(src_file)] 168 | 169 | if not args.srcfiles: 170 | print("No files to parse. Aborting.", file=sys.stderr) 171 | sys.exit(-1) 172 | 173 | try: 174 | mdimggen = MarkdownImageGen(args) 175 | mdimggen.processFiles(args.srcfiles) 176 | except OSError as e: 177 | print(e) 178 | sys.exit(-1) 179 | except KeyboardInterrupt as e: 180 | print(" Aborting.", file=sys.stderr) 181 | sys.exit(-1) 182 | 183 | if errorlog.has_errors: 184 | print("WARNING: Errors encountered.", file=sys.stderr) 185 | sys.exit(-1) 186 | 187 | sys.exit(0) 188 | 189 | 190 | if __name__ == "__main__": 191 | mdimggen_main() 192 | 193 | 194 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 195 | -------------------------------------------------------------------------------- /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 .filehashes import FileHashes 13 | 14 | 15 | class OriginInfo: 16 | def __init__(self, file, line): 17 | self.file = file 18 | self.line = line 19 | 20 | @property 21 | def md_file(self): 22 | if self._md_in_links: 23 | return self.file+".md" 24 | return self.file 25 | 26 | 27 | class DocsGenParser(object): 28 | _header_pat = re.compile(r"^// ([A-Z][A-Za-z0-9_&-]*( ?[A-Z][A-Za-z0-9_&-]*)?)(\([^)]*\))?:( .*)?$") 29 | RCFILE = ".openscad_docsgen_rc" 30 | HASHFILE = ".source_hashes" 31 | 32 | def __init__(self, opts): 33 | self.opts = opts 34 | self.target = opts.target 35 | self.strict = opts.strict 36 | self.quiet = opts.quiet 37 | self.file_blocks = [] 38 | self.curr_file_block = None 39 | self.curr_section = None 40 | self.curr_item = None 41 | self.curr_parent = None 42 | self.ignored_file_pats = [] 43 | self.ignored_files = {} 44 | self.priority_files = [] 45 | self.priority_groups = [] 46 | self.items_by_name = {} 47 | self.definitions = {} 48 | self.defn_aliases = {} 49 | self.syntags_data = {} 50 | 51 | sfx = self.target.get_suffix() 52 | self.TOCFILE = "TOC" + sfx 53 | self.TOPICFILE = "Topics" + sfx 54 | self.INDEXFILE = "AlphaIndex" + sfx 55 | self.GLOSSARYFILE = "Glossary" + sfx 56 | self.CHEATFILE = "CheatSheet" + sfx 57 | self.SIDEBARFILE = "_Sidebar" + sfx 58 | 59 | self._reset_header_defs() 60 | 61 | def _reset_header_defs(self): 62 | self.header_defs = { 63 | # BlockHeader: (parenttype, nodetype, extras, callback) 64 | 'Status': ( ItemBlock, LabelBlock, None, self._status_block_cb ), 65 | 'Alias': ( ItemBlock, LabelBlock, None, self._alias_block_cb ), 66 | 'Aliases': ( ItemBlock, LabelBlock, None, self._alias_block_cb ), 67 | 'Arguments': ( ItemBlock, TableBlock, ( 68 | ('^By Position', 'What it does'), 69 | ('^By Name', 'What it does') 70 | ), None 71 | ), 72 | } 73 | lines = [ 74 | "// DefineHeader(Headerless): Continues", 75 | "// DefineHeader(Text;ItemOnly): Description", 76 | "// DefineHeader(BulletList;ItemOnly): Usage", 77 | ] 78 | self.parse_lines(lines, src_file="Defaults") 79 | if os.path.exists(self.RCFILE): 80 | with open(self.RCFILE, "r") as f: 81 | lines = ["// " + line for line in f.readlines()] 82 | self.parse_lines(lines, src_file=self.RCFILE) 83 | 84 | def _status_block_cb(self, title, subtitle, body, origin, meta): 85 | self.curr_item.deprecated = "DEPRECATED" in subtitle 86 | 87 | def _alias_block_cb(self, title, subtitle, body, origin, meta): 88 | aliases = [x.strip() for x in subtitle.split(",")] 89 | self.curr_item.aliases.extend(aliases) 90 | for alias in aliases: 91 | self.items_by_name[alias] = self.curr_item 92 | 93 | def _skip_lines(self, lines, line_num=0): 94 | while line_num < len(lines): 95 | line = lines[line_num] 96 | if self.curr_item and not line.startswith("//"): 97 | self.curr_parent = self.curr_item.parent 98 | self.curr_item = None 99 | match = self._header_pat.match(line) 100 | if match: 101 | return line_num 102 | line_num += 1 103 | if self.curr_item: 104 | self.curr_parent = self.curr_item.parent 105 | self.curr_item = None 106 | return line_num 107 | 108 | def _files_prioritized(self): 109 | out = [] 110 | found = {} 111 | for pri_file in self.priority_files: 112 | for file_block in self.file_blocks: 113 | if file_block.subtitle == pri_file: 114 | found[pri_file] = True 115 | out.append(file_block) 116 | for file_block in self.file_blocks: 117 | if file_block.subtitle not in found: 118 | out.append(file_block) 119 | return out 120 | 121 | def _parse_meta_dict(self, meta): 122 | meta_dict = {} 123 | for part in meta.split(';'): 124 | if "=" in part: 125 | key, val = part.split('=',1) 126 | else: 127 | key, val = part, 1 128 | meta_dict[key] = val 129 | return meta_dict 130 | 131 | def _define_blocktype(self, title, meta): 132 | title = title.strip() 133 | parentspec = None 134 | meta = self._parse_meta_dict(meta) 135 | 136 | if "ItemOnly" in meta: 137 | parentspec = ItemBlock 138 | 139 | if "NumList" in meta: 140 | self.header_defs[title] = (parentspec, NumberedListBlock, None, None) 141 | elif "BulletList" in meta: 142 | self.header_defs[title] = (parentspec, BulletListBlock, None, None) 143 | elif "Table" in meta: 144 | if "Headers" not in meta: 145 | raise DocsGenException("DefineHeader", "Table type is missing Header= option, while declaring block:") 146 | hdr_meta = meta["Headers"].split("||") 147 | hdr_sets = [[x.strip() for x in hset.split("|")] for hset in hdr_meta] 148 | self.header_defs[title] = (parentspec, TableBlock, hdr_sets, None) 149 | elif "Example" in meta: 150 | self.header_defs[title] = (parentspec, ExampleBlock, None, None) 151 | elif "Figure" in meta: 152 | self.header_defs[title] = (parentspec, FigureBlock, None, None) 153 | elif "Label" in meta: 154 | self.header_defs[title] = (parentspec, LabelBlock, None, None) 155 | elif "Headerless" in meta: 156 | self.header_defs[title] = (parentspec, HeaderlessBlock, None, None) 157 | elif "Text" in meta: 158 | self.header_defs[title] = (parentspec, TextBlock, None, None) 159 | elif "Generic" in meta: 160 | self.header_defs[title] = (parentspec, GenericBlock, None, None) 161 | else: 162 | raise DocsGenException("DefineHeader", "Could not parse target block type, while declaring block:") 163 | 164 | def _check_filenode(self, title, origin): 165 | if not self.curr_file_block: 166 | raise DocsGenException(title, "Must declare File or Libfile block before declaring block:") 167 | 168 | def _parse_block(self, lines, line_num=0, src_file=None): 169 | line_num = self._skip_lines(lines, line_num) 170 | if line_num >= len(lines): 171 | return line_num 172 | hdr_line_num = line_num 173 | line = lines[line_num] 174 | match = self._header_pat.match(line) 175 | if not match: 176 | return line_num 177 | title = match.group(1) 178 | meta = match.group(3)[1:-1] if match.group(3) else "" 179 | subtitle = match.group(4).strip() if match.group(4) else "" 180 | body = [] 181 | unstripped_body = [] 182 | line_num += 1 183 | 184 | try: 185 | first_line = True 186 | indent = 2 187 | while line_num < len(lines): 188 | line = lines[line_num] 189 | if not line.startswith("//" + (" " * indent)): 190 | if line.startswith("// "): 191 | raise DocsGenException(title, "Body line has less indentation than first line, while declaring block:") 192 | break 193 | line = line[2:] 194 | if first_line: 195 | first_line = False 196 | indent = len(line) - len(line.lstrip()) 197 | line = line[indent:] 198 | unstripped_body.append(line.rstrip('\n')) 199 | body.append(line.rstrip()) 200 | line_num += 1 201 | 202 | parent = self.curr_parent 203 | origin = OriginInfo(src_file, hdr_line_num+1) 204 | if title == "DefineHeader": 205 | self._define_blocktype(subtitle, meta) 206 | elif title == "IgnoreFiles": 207 | if origin.file != self.RCFILE: 208 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 209 | if subtitle: 210 | body.insert(0,subtitle) 211 | self.ignored_file_pats.extend([ 212 | fname.strip() for fname in body 213 | ]) 214 | self.ignored_files = {} 215 | for pat in self.ignored_file_pats: 216 | files = glob.glob(pat,recursive=True) 217 | for fname in files: 218 | self.ignored_files[fname] = True 219 | elif title == "PrioritizeFiles": 220 | if origin.file != self.RCFILE: 221 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 222 | if subtitle: 223 | body.insert(0,subtitle) 224 | self.priority_files = [x for line in body for x in glob.glob(line.strip())] 225 | elif title == "DocsDirectory": 226 | if origin.file != self.RCFILE: 227 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 228 | if body: 229 | raise DocsGenException(title, "Body not supported, while declaring block:") 230 | self.opts.docs_dir = subtitle.strip().rstrip("/") 231 | self.opts.update_target() 232 | elif title == "UsePNGAnimations": 233 | if origin.file != self.RCFILE: 234 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 235 | if body: 236 | raise DocsGenException(title, "Body not supported, while declaring block:") 237 | self.opts.png_animation = (subtitle.strip().upper() in ["TRUE", "YES", "1"]) 238 | self.opts.update_target() 239 | elif title == "ProjectName": 240 | if origin.file != self.RCFILE: 241 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 242 | if body: 243 | raise DocsGenException(title, "Body not supported, while declaring block:") 244 | self.opts.project_name = subtitle.strip() 245 | self.opts.update_target() 246 | elif title == "TargetProfile": 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 | if not self.opts.set_target(subtitle.strip()): 252 | raise DocsGenException(title, "Body not supported, while declaring block:") 253 | self.opts.target_profile = subtitle.strip() 254 | self.opts.update_target() 255 | elif title == "GenerateDocs": 256 | if origin.file != self.RCFILE: 257 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 258 | if body: 259 | raise DocsGenException(title, "Body not supported, while declaring block:") 260 | if not ( 261 | self.opts.gen_files or 262 | self.opts.gen_toc or 263 | self.opts.gen_index or 264 | self.opts.gen_topics or 265 | self.opts.gen_cheat or 266 | self.opts.gen_sidebar or 267 | self.opts.gen_glossary 268 | ): 269 | # Only use default GeneratedDocs if the command-line doesn't specify any docs 270 | # types to generate. 271 | for part in subtitle.split(","): 272 | orig_part = part.strip() 273 | part = orig_part.upper() 274 | if part == "FILES": 275 | self.opts.gen_files = True 276 | elif part == "TOC": 277 | self.opts.gen_toc = True 278 | elif part == "INDEX": 279 | self.opts.gen_index = True 280 | elif part == "TOPICS": 281 | self.opts.gen_topics = True 282 | elif part in ["CHEAT", "CHEATSHEET"]: 283 | self.opts.gen_cheat = True 284 | elif part == "GLOSSARY": 285 | self.opts.gen_glossary = True 286 | elif part == "SIDEBAR": 287 | self.opts.gen_sidebar = True 288 | else: 289 | raise DocsGenException(title, 'Unknown type "{}", while declaring block:'.format(orig_part)) 290 | elif title == "SidebarHeader": 291 | if origin.file != self.RCFILE: 292 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 293 | body = unstripped_body 294 | if subtitle: 295 | body.insert(0,subtitle) 296 | body = [line[1:] if line.startswith(".") else line for line in body] 297 | self.opts.sidebar_header = body 298 | elif title == "SidebarMiddle": 299 | if origin.file != self.RCFILE: 300 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 301 | body = unstripped_body 302 | if subtitle: 303 | body.insert(0,subtitle) 304 | body = [line[1:] if line.startswith(".") else line for line in body] 305 | self.opts.sidebar_middle = body 306 | elif title == "SidebarFooter": 307 | if origin.file != self.RCFILE: 308 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 309 | body = unstripped_body 310 | if subtitle: 311 | body.insert(0,subtitle) 312 | body = [line[1:] if line.startswith(".") else line for line in body] 313 | self.opts.sidebar_footer = body 314 | elif title == "DefineSynTags": 315 | if origin.file != self.RCFILE: 316 | raise DocsGenException(title, "Block disallowed outside of {} file:".format(self.RCFILE)) 317 | if subtitle: 318 | raise DocsGenException(title, "Subtitle not supported, while declaring block:") 319 | for line in body: 320 | if '=' not in line: 321 | raise DocsGenException(title, "Malformed tag definition '{}' while declaring block:".format(line)) 322 | tag, text = [x.strip() for x in line.split("=",1)] 323 | self.syntags_data[tag] = text 324 | elif title == "vim" or title == "emacs": 325 | pass # Ignore vim and emacs modelines 326 | elif title in ["File", "LibFile"]: 327 | if self.curr_file_block: 328 | raise DocsGenException(title, "File or Libfile must be the first block specified, and must be specified at most once. Encountered while declaring block:") 329 | self.curr_file_block = FileBlock(title, subtitle, body, origin) 330 | self.curr_section = None 331 | self.curr_subsection = None 332 | self.curr_parent = self.curr_file_block 333 | self.file_blocks.append(self.curr_file_block) 334 | elif not self.curr_file_block and self.strict: 335 | raise DocsGenException(title, "Must declare File or LibFile block before declaring block:") 336 | 337 | elif title == "Section": 338 | self._check_filenode(title, origin) 339 | self.curr_section = SectionBlock(title, subtitle, body, origin, parent=self.curr_file_block) 340 | self.curr_subsection = None 341 | self.curr_parent = self.curr_section 342 | elif title == "Subsection": 343 | if not self.curr_section: 344 | raise DocsGenException(title, "Must declare a Section before declaring block:") 345 | if not subtitle: 346 | raise DocsGenException(title, "Must provide a subtitle when declaring block:") 347 | self.curr_subsection = SubsectionBlock(title, subtitle, body, origin, parent=self.curr_section) 348 | self.curr_parent = self.curr_subsection 349 | elif title == "Includes": 350 | self._check_filenode(title, origin) 351 | IncludesBlock(title, subtitle, body, origin, parent=self.curr_file_block) 352 | elif title == "FileSummary": 353 | if not subtitle: 354 | raise DocsGenException(title, "Must provide a subtitle when declaring block:") 355 | self._check_filenode(title, origin) 356 | self.curr_file_block.summary = subtitle.strip() 357 | elif title == "FileGroup": 358 | if not subtitle: 359 | raise DocsGenException(title, "Must provide a subtitle when declaring block:") 360 | self._check_filenode(title, origin) 361 | self.curr_file_block.group = subtitle.strip() 362 | elif title == "FileFootnotes": 363 | if not subtitle: 364 | raise DocsGenException(title, "Must provide a subtitle when declaring block:") 365 | self._check_filenode(title, origin) 366 | self.curr_file_block.footnotes = [] 367 | for part in subtitle.split(";"): 368 | fndata = [x.strip() for x in part.strip().split('=',1)] 369 | fndata.append(origin) 370 | self.curr_file_block.footnotes.append(fndata) 371 | elif title == "CommonCode": 372 | self._check_filenode(title, origin) 373 | self.curr_file_block.common_code.extend(body) 374 | elif title == "Definitions": 375 | print("DEF") 376 | self._check_filenode(title, origin) 377 | block = DefinitionsBlock(title, subtitle, body, origin, parent=parent) 378 | for main_term, info in block.definitions.items(): 379 | terms, defn = info 380 | for term in terms: 381 | if term in self.definitions or term in self.defn_aliases: 382 | raise DocsGenException(title, 'Term "{}" re-defined, while declaring block:'.format(term)) 383 | self.definitions[main_term] = (terms, defn) 384 | for term in terms[1:]: 385 | self.defn_aliases[term] = main_term 386 | elif title == "Figure": 387 | self._check_filenode(title, origin) 388 | FigureBlock(title, subtitle, body, origin, parent=parent, meta=meta, use_apngs=self.opts.png_animation) 389 | elif title == "Example": 390 | if self.curr_item: 391 | ExampleBlock(title, subtitle, body, origin, parent=parent, meta=meta, use_apngs=self.opts.png_animation) 392 | elif title == "Figures": 393 | self._check_filenode(title, origin) 394 | for lnum, line in enumerate(body): 395 | FigureBlock("Figure", subtitle, [line], origin, parent=parent, meta=meta, use_apngs=self.opts.png_animation) 396 | subtitle = "" 397 | elif title == "Examples": 398 | if self.curr_item: 399 | for lnum, line in enumerate(body): 400 | ExampleBlock("Example", subtitle, [line], origin, parent=parent, meta=meta, use_apngs=self.opts.png_animation) 401 | subtitle = "" 402 | elif title in self.header_defs: 403 | parcls, cls, data, cb = self.header_defs[title] 404 | if not parcls or isinstance(self.curr_parent, parcls): 405 | if cls in (GenericBlock, LabelBlock, TextBlock, HeaderlessBlock, NumberedListBlock, BulletListBlock): 406 | cls(title, subtitle, body, origin, parent=parent) 407 | elif cls == TableBlock: 408 | cls(title, subtitle, body, origin, parent=parent, header_sets=data) 409 | elif cls in (FigureBlock, ExampleBlock): 410 | cls(title, subtitle, body, origin, parent=parent, meta=meta, use_apngs=self.opts.png_animation) 411 | if cb: 412 | cb(title, subtitle, body, origin, meta) 413 | 414 | elif title in ["Constant", "Function", "Module", "Function&Module"]: 415 | self._check_filenode(title, origin) 416 | if not self.curr_section: 417 | self.curr_section = SectionBlock("Section", "", [], origin, parent=self.curr_file_block) 418 | parent = self.curr_parent = self.curr_section 419 | if subtitle in self.items_by_name: 420 | prevorig = self.items_by_name[subtitle].origin 421 | msg = "Previous declaration of `{}` at {}:{}, Redeclared:".format(subtitle, prevorig.file, prevorig.line) 422 | raise DocsGenException(title, msg) 423 | item = ItemBlock(title, subtitle, body, origin, parent=parent) 424 | self.items_by_name[subtitle] = item 425 | self.curr_item = item 426 | self.curr_parent = item 427 | elif title == "Synopsis": 428 | if self.curr_item: 429 | SynopsisBlock(title, subtitle, body, origin, parent=parent) 430 | elif title == "SynTags": 431 | if self.curr_item: 432 | SynTagsBlock(title, subtitle, body, origin, parent=parent, syntags_data=self.syntags_data) 433 | elif title == "Topics": 434 | if self.curr_item: 435 | TopicsBlock(title, subtitle, body, origin, parent=parent) 436 | elif title == "See Also": 437 | if self.curr_item: 438 | SeeAlsoBlock(title, subtitle, body, origin, parent=parent) 439 | else: 440 | raise DocsGenException(title, "Unrecognized block:") 441 | 442 | if line_num >= len(lines) or not lines[line_num].startswith("//"): 443 | if self.curr_item: 444 | self.curr_parent = self.curr_item.parent 445 | self.curr_item = None 446 | line_num = self._skip_lines(lines, line_num) 447 | 448 | except DocsGenException as e: 449 | errorlog.add_entry(origin.file, origin.line, str(e), ErrorLog.FAIL) 450 | 451 | return line_num 452 | 453 | def get_indexed_names(self): 454 | """Returns the list of all indexable function/module/constants by name, in alphabetical order. 455 | """ 456 | lst = sorted(self.items_by_name.keys()) 457 | for item in lst: 458 | yield item 459 | 460 | def get_indexed_data(self, name): 461 | """Given the name of an indexable function/module/constant, returns the parsed data dictionary for that item's documentation. 462 | 463 | Example Results 464 | --------------- 465 | { 466 | "name": "Function&Module", 467 | "subtitle": "foobar()", 468 | "body": [], 469 | "file": "foobar.scad", 470 | "line": 23, 471 | "topics": ["Testing", "Metasyntactic"], 472 | "aliases": ["foob()", "feeb()"], 473 | "see_also": ["barbaz()", "bazqux()"], 474 | "synopsis": "This function does bar.", 475 | "syntags": { 476 | "VNF": "Returns a VNF when called as a function.", 477 | "Geom": "Returns Geometry when called as a module." 478 | }, 479 | "usages": [ 480 | { 481 | "subtitle": "As function", 482 | "body": [ 483 | "val = foobar(a, b, );", 484 | "list = foobar(d, b=);" 485 | ] 486 | }, { 487 | "subtitle": "As module", 488 | "body": [ 489 | "foobar(a, b, );", 490 | "foobar(d, b=);" 491 | ] 492 | } 493 | ], 494 | "description": [ 495 | "When called as a function, this returns the foo of bar.", 496 | "When called as a module, renders a foo as modified by bar." 497 | ], 498 | "arguments": [ 499 | "a = The a argument.", 500 | "b = The b argument.", 501 | "c = The c argument.", 502 | "d = The d argument." 503 | ], 504 | "examples": [ 505 | [ 506 | "foobar(5, 7)" 507 | ], [ 508 | "x = foobar(5, 7);", 509 | "echo(x);" 510 | ] 511 | ] 512 | "children": [ 513 | { 514 | "name": "Extra Anchors", 515 | "subtitle": "", 516 | "body": [ 517 | "\"fee\" = Anchors at the fee position.", 518 | "\"fie\" = Anchors at the fie position." 519 | ] 520 | } 521 | ] 522 | } 523 | """ 524 | if name in self.items_by_name: 525 | return self.items_by_name[name].get_data() 526 | return {} 527 | 528 | def get_all_data(self): 529 | """Gets all the documentation data parsed so far. 530 | 531 | Sample Results 532 | ---------- 533 | [ 534 | { 535 | "name": "LibFile", 536 | "subtitle":"foobar.scad", 537 | "body": [ 538 | "This is the first line of the LibFile body.", 539 | "This is the second line of the LibFile body." 540 | ], 541 | "includes": [ 542 | "include ", 543 | "include " 544 | ], 545 | "commoncode": [ 546 | "$fa = 2;", 547 | "$fs = 2;" 548 | ], 549 | "children": [ 550 | { 551 | "name": "Section", 552 | "subtitle": "Metasyntactical Calls", // If subtitle is "", section is just a placeholder. 553 | "body": [ 554 | "This is the first line of the body of the Section.", 555 | "This is the second line of the body of the Section." 556 | ], 557 | "children": [ 558 | { 559 | "name": "Function&Module", 560 | "subtitle": "foobar()", 561 | "body": [], 562 | "file": "foobar.scad", 563 | "line": 23, 564 | "topics": ["Testing", "Metasyntactic"], 565 | "aliases": ["foob()", "feeb()"], 566 | "see_also": ["barbaz()", "bazqux()"], 567 | "synopsis": "This function does bar.", 568 | "syntags": { 569 | "VNF": "Returns a VNF when called as a function.", 570 | "Geom": "Returns Geometry when called as a module." 571 | }, 572 | "usages": [ 573 | { 574 | "subtitle": "As function", 575 | "body": [ 576 | "val = foobar(a, b, );", 577 | "list = foobar(d, b=);" 578 | ] 579 | }, { 580 | "subtitle": "As module", 581 | "body": [ 582 | "foobar(a, b, );", 583 | "foobar(d, b=);" 584 | ] 585 | } 586 | ], 587 | "description": [ 588 | "When called as a function, this returns the foo of bar.", 589 | "When called as a module, renders a foo as modified by bar." 590 | ], 591 | "arguments": [ 592 | "a = The a argument.", 593 | "b = The b argument.", 594 | "c = The c argument.", 595 | "d = The d argument." 596 | ], 597 | "examples": [ 598 | [ 599 | "foobar(5, 7)" 600 | ], 601 | [ 602 | "x = foobar(5, 7);", 603 | "echo(x);" 604 | ], 605 | // ... Next Example 606 | ] 607 | "children": [ 608 | { 609 | "name": "Extra Anchors", 610 | "subtitle": "", 611 | "body": [ 612 | "\"fee\" = Anchors at the fee position.", 613 | "\"fie\" = Anchors at the fie position." 614 | ] 615 | } 616 | ] 617 | }, 618 | // ... next function/module/constant 619 | ] 620 | }, 621 | // ... next section 622 | ] 623 | }, 624 | // ... next file 625 | ] 626 | """ 627 | return [ 628 | fblock.get_data() 629 | for fblock in self.file_blocks 630 | ] 631 | 632 | def parse_lines(self, lines, line_num=0, src_file=None): 633 | """Parses the given list of strings for documentation comments. 634 | 635 | Parameters 636 | ---------- 637 | lines : list of str 638 | The list of strings to parse for documentation comments. 639 | line_num : int 640 | The current index into the list of strings of the current line to parse. 641 | src_file : str 642 | The name of the source file that this is from. This is used just for error reporting. 643 | If true, generates images for example scripts, by running them in OpenSCAD. 644 | """ 645 | while line_num < len(lines): 646 | line_num = self._parse_block(lines, line_num, src_file=src_file) 647 | 648 | def parse_file(self, filename, commentless=False): 649 | """Parses the given file for documentation comments. 650 | 651 | Parameters 652 | ---------- 653 | filename : str 654 | The name of the file to parse documentaiton comments from. 655 | commentless : bool 656 | If true, treat every line of the file as if it starts with '// '. This is used for reading docsgen config files. 657 | """ 658 | if filename in self.ignored_files: 659 | return 660 | if not self.quiet: 661 | print(" {}".format(filename), end='') 662 | sys.stdout.flush() 663 | self.curr_file_block = None 664 | self.curr_section = None 665 | self._reset_header_defs() 666 | with open(filename, "r") as f: 667 | if commentless: 668 | lines = ["// " + line for line in f.readlines()] 669 | else: 670 | lines = f.readlines() 671 | self.parse_lines(lines, src_file=filename) 672 | 673 | def parse_files(self, filenames, commentless=False): 674 | """Parses all of the given files for documentation comments. 675 | 676 | Parameters 677 | ---------- 678 | filenames : list of str 679 | The list of filenames to parse documentaiton comments from. 680 | commentless : bool 681 | If true, treat every line of the files as if they starts with '// '. This is used for reading docsgen config files. 682 | """ 683 | if not self.quiet: 684 | print("Parsing...") 685 | print(" ", end='') 686 | col = 1 687 | for filename in filenames: 688 | if filename in self.ignored_files: 689 | continue 690 | flen = len(filename) + 1 691 | if col > 1 and flen + col >= 79: 692 | print("") 693 | print(" ", end='') 694 | col = 1 695 | self.parse_file(filename, commentless=commentless) 696 | col = col + flen 697 | for key, info in self.definitions.items(): 698 | keys, defn = info 699 | blk = self.file_blocks[0] 700 | defn = blk.parse_links(defn, self, self.target, html=False) 701 | self.definitions[key] = (keys, defn) 702 | if not self.quiet: 703 | print("") 704 | 705 | def dump_tree(self, nodes, pfx="", maxdepth=6): 706 | """Dumps debug info to stdout for parsed documentation subtree.""" 707 | if maxdepth <= 0 or not nodes: 708 | return 709 | for node in nodes: 710 | print("{}{}".format(pfx,node)) 711 | for line in node.body: 712 | print(" {}{}".format(pfx,line)) 713 | self.dump_tree(node.children, pfx=pfx+" ", maxdepth=maxdepth-1) 714 | 715 | def dump_full_tree(self): 716 | """Dumps debug info to stdout for all parsed documentation.""" 717 | self.dump_tree(self.file_blocks) 718 | 719 | def write_docs_files(self): 720 | """Generates the docs files for each source file that has been parsed. 721 | """ 722 | target = self.opts.target 723 | if self.opts.test_only: 724 | for fblock in sorted(self.file_blocks, key=lambda x: x.subtitle.strip()): 725 | lines = fblock.get_file_lines(self, target) 726 | image_manager.process_requests(test_only=True) 727 | return 728 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 729 | filehashes = FileHashes(os.path.join(target.docs_dir, self.HASHFILE)) 730 | for fblock in sorted(self.file_blocks, key=lambda x: x.subtitle.strip()): 731 | outfile = os.path.join(target.docs_dir, fblock.origin.file+target.get_suffix()) 732 | if not self.quiet: 733 | print("Writing {}...".format(outfile)) 734 | outdir = os.path.dirname(outfile) 735 | if not os.path.exists(outdir): 736 | os.makedirs(outdir, mode=0o744, exist_ok=True) 737 | out = fblock.get_file_lines(self, target) 738 | out = target.postprocess(out) 739 | with open(outfile,"w") as f: 740 | for line in out: 741 | f.write(line + "\n") 742 | if self.opts.gen_imgs: 743 | filename = fblock.subtitle.strip() 744 | has_changed = filehashes.is_changed(filename) 745 | if self.opts.force or has_changed: 746 | image_manager.process_requests(test_only=False) 747 | image_manager.purge_requests() 748 | if errorlog.file_has_errors(filename): 749 | filehashes.invalidate(filename) 750 | filehashes.save() 751 | 752 | def write_toc_file(self): 753 | """Generates the table-of-contents TOC file from the parsed documentation""" 754 | target = self.opts.target 755 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 756 | prifiles = self._files_prioritized() 757 | groups = [] 758 | for fblock in prifiles: 759 | if fblock.group and fblock.group not in groups: 760 | groups.append(fblock.group) 761 | for fblock in prifiles: 762 | if not fblock.group and fblock.group not in groups: 763 | groups.append(fblock.group) 764 | 765 | footmarks = [] 766 | footnotes = {} 767 | out = target.header("Table of Contents") 768 | out.extend(target.header("List of Files", lev=target.SECTION)) 769 | for group in groups: 770 | out.extend(target.block_header(group if group else "Miscellaneous")) 771 | out.extend(target.bullet_list_start()) 772 | for fnum, fblock in enumerate(prifiles): 773 | if fblock.group != group: 774 | continue 775 | file = fblock.subtitle 776 | anch = target.header_link("{}. {}".format(fnum+1, file)) 777 | link = target.get_link(file, anchor=anch, literalize=False) 778 | filelink = target.get_link("docs", file=file, literalize=False) 779 | tags = {tag: text for tag, text, origin in fblock.footnotes} 780 | marks = target.mouseover_tags(tags, "#file-footnotes") 781 | out.extend(target.bullet_list_item("{} ({}){}".format(link, filelink, marks))) 782 | out.append(fblock.summary) 783 | for mark, note, origin in fblock.footnotes: 784 | try: 785 | if mark not in footmarks: 786 | footmarks.append(mark) 787 | if mark not in footnotes: 788 | footnotes[mark] = note 789 | elif note != footnotes[mark]: 790 | raise DocsGenException("FileFootnotes", 'Footnote "{}" conflicts with previous definition "{}", while declaring block:'.format(note, footnotes[mark])) 791 | except DocsGenException as e: 792 | errorlog.add_entry(origin.file, origin.line, str(e), ErrorLog.FAIL) 793 | out.extend(target.bullet_list_end()) 794 | 795 | if footmarks: 796 | out.append("") 797 | out.extend(target.header("File Footnotes:", lev=target.SUBSECTION)) 798 | for mark in footmarks: 799 | out.append("{} = {} ".format(mark, note)) 800 | out.append("") 801 | 802 | for fnum, fblock in enumerate(prifiles): 803 | out.extend(fblock.get_tocfile_lines(self, self.opts.target, n=fnum+1, currfile=self.TOCFILE)) 804 | 805 | out = target.postprocess(out) 806 | outfile = os.path.join(target.docs_dir, self.TOCFILE) 807 | if not self.quiet: 808 | print("Writing {}...".format(outfile)) 809 | with open(outfile, "w") as f: 810 | for line in out: 811 | f.write(line + "\n") 812 | 813 | def write_glossary_file(self): 814 | """Generates the Glossary file from the parsed documentation.""" 815 | target = self.opts.target 816 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 817 | defs = {key: info[1] for key, info in self.definitions.items()} 818 | sorted_words = sorted(list(defs.keys()), key=lambda v: v.upper()) 819 | ltrs_found = {} 820 | for word in sorted_words: 821 | ltr = word[0].upper() 822 | ltrs_found[ltr] = 1 823 | ltrs_found = sorted(list(ltrs_found.keys())) 824 | 825 | out = [] 826 | out = target.header("Glossary") 827 | out.extend(target.markdown_block([ 828 | "Definitions of various Words and terms." 829 | ])) 830 | out.extend(target.markdown_block([ 831 | " ".join( 832 | target.get_link(ltr.upper(), anchor=ltr.lower(), literalize=False) 833 | for ltr in ltrs_found 834 | ) 835 | ])) 836 | out.extend(target.horizontal_rule()) 837 | old_ltr = '' 838 | for word in sorted_words: 839 | ltr = word[0].upper() 840 | if old_ltr != ltr: 841 | out.extend(target.header(ltr.upper(), lev=2)) 842 | old_ltr = ltr 843 | defn = defs[word.lower()] 844 | out.extend(target.header(word.title(), lev=3)) 845 | out.extend(target.markdown_block([defn])) 846 | out = target.postprocess(out) 847 | outfile = os.path.join(target.docs_dir, self.GLOSSARYFILE) 848 | if not self.quiet: 849 | print("Writing {}...".format(outfile)) 850 | with open(outfile, "w") as f: 851 | for line in out: 852 | f.write(line + "\n") 853 | 854 | def write_topics_file(self): 855 | """Generates the Topics file from the parsed documentation.""" 856 | target = self.opts.target 857 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 858 | index_by_letter = {} 859 | for file_block in self.file_blocks: 860 | for section in file_block.children: 861 | if not isinstance(section,SectionBlock): 862 | continue 863 | for item in section.children: 864 | if not isinstance(item,ItemBlock): 865 | continue 866 | names = [item.subtitle] 867 | names.extend(item.aliases) 868 | for topic in item.topics: 869 | ltr = "0" if not topic[0].isalpha() else topic[0].upper() 870 | if ltr not in index_by_letter: 871 | index_by_letter[ltr] = {} 872 | if topic not in index_by_letter[ltr]: 873 | index_by_letter[ltr][topic] = [] 874 | for name in names: 875 | index_by_letter[ltr][topic].append( (name, item) ) 876 | ltrs_found = sorted(index_by_letter.keys()) 877 | out = target.header("Topic Index") 878 | out.extend(target.markdown_block([ 879 | "An index of topics, with related functions, modules, and constants." 880 | ])) 881 | for ltr in ltrs_found: 882 | out.extend( 883 | target.markdown_block([ 884 | "{}: {}".format( 885 | target.bold(ltr), 886 | ", ".join( 887 | target.get_link( 888 | target.escape_entities(topic), 889 | anchor=target.header_link(topic), 890 | literalize=False 891 | ) 892 | for topic in sorted(index_by_letter[ltr].keys()) 893 | ) 894 | ) 895 | ]) 896 | ) 897 | for ltr in ltrs_found: 898 | topics = sorted(index_by_letter[ltr].keys()) 899 | for topic in topics: 900 | itemlist = index_by_letter[ltr][topic] 901 | out.extend(target.header(topic, lev=target.ITEM)) 902 | out.extend(target.bullet_list_start()) 903 | sorted_items = sorted(itemlist, key=lambda x: x[0].lower()) 904 | for name, item in sorted_items: 905 | out.extend( 906 | target.bullet_list_item( 907 | item.get_index_line(self, target, self.TOPICFILE) 908 | ) 909 | ) 910 | out.extend(target.bullet_list_end()) 911 | 912 | out = target.postprocess(out) 913 | outfile = os.path.join(target.docs_dir, self.TOPICFILE) 914 | if not self.quiet: 915 | print("Writing {}...".format(outfile)) 916 | with open(outfile, "w") as f: 917 | for line in out: 918 | f.write(line + "\n") 919 | 920 | def write_index_file(self): 921 | """Generates the alphabetical function/module/constant AlphaIndex file from the parsed documentation.""" 922 | target = self.opts.target 923 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 924 | unsorted_items = [] 925 | for file_block in self.file_blocks: 926 | for sect in file_block.get_children_by_title("Section"): 927 | items = [ 928 | item for item in sect.children 929 | if isinstance(item, ItemBlock) 930 | ] 931 | for item in items: 932 | names = [item.subtitle] 933 | names.extend(item.aliases) 934 | for name in names: 935 | unsorted_items.append( (name, item) ) 936 | sorted_items = sorted(unsorted_items, key=lambda x: x[0].lower()) 937 | index_by_letter = {} 938 | for name, item in sorted_items: 939 | ltr = "0" if not name[0].isalpha() else name[0].upper() 940 | if ltr not in index_by_letter: 941 | index_by_letter[ltr] = [] 942 | index_by_letter[ltr].append( (name, item ) ) 943 | ltrs_found = sorted(index_by_letter.keys()) 944 | out = target.header("Alphabetical Index") 945 | out.extend(target.markdown_block([ 946 | "An index of Functions, Modules, and Constants by name.", 947 | ])) 948 | out.extend(target.markdown_block([ 949 | " ".join( 950 | target.get_link(ltr, anchor=ltr.lower(), literalize=False) 951 | for ltr in ltrs_found 952 | ) 953 | ])) 954 | for ltr in ltrs_found: 955 | items = [ 956 | item.get_index_line(self, target, self.INDEXFILE) 957 | for name, item in index_by_letter[ltr] 958 | ] 959 | out.extend(target.header(ltr, lev=target.SUBSECTION)) 960 | out.extend(target.bullet_list(items)) 961 | 962 | out = target.postprocess(out) 963 | outfile = os.path.join(target.docs_dir, self.INDEXFILE) 964 | if not self.quiet: 965 | print("Writing {}...".format(outfile)) 966 | with open(outfile, "w") as f: 967 | for line in out: 968 | f.write(line + "\n") 969 | 970 | def write_cheatsheet_file(self): 971 | """Generates the CheatSheet file from the parsed documentation.""" 972 | target = self.opts.target 973 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 974 | if target.project_name is None: 975 | title = "Cheat Sheet" 976 | else: 977 | title = "{} Cheat Sheet".format(target.project_name) 978 | out = target.header(title) 979 | pri_blocks = self._files_prioritized() 980 | for file_block in pri_blocks: 981 | out.extend(file_block.get_cheatsheet_lines(self, self.opts.target)) 982 | 983 | out = target.postprocess(out) 984 | outfile = os.path.join(target.docs_dir, self.CHEATFILE) 985 | if not self.quiet: 986 | print("Writing {}...".format(outfile)) 987 | with open(outfile, "w") as f: 988 | for line in out: 989 | f.write(line + "\n") 990 | 991 | def write_sidebar_file(self): 992 | """Generates the _Sidebar index of files from the parsed documentation""" 993 | target = self.opts.target 994 | os.makedirs(target.docs_dir, mode=0o744, exist_ok=True) 995 | prifiles = self._files_prioritized() 996 | groups = [] 997 | for fblock in prifiles: 998 | if fblock.group and fblock.group not in groups: 999 | groups.append(fblock.group) 1000 | for fblock in prifiles: 1001 | if not fblock.group and fblock.group not in groups: 1002 | groups.append(fblock.group) 1003 | 1004 | footmarks = [] 1005 | footnotes = {} 1006 | out = [] 1007 | if self.opts.sidebar_header: 1008 | out.extend(self.opts.sidebar_header) 1009 | if self.opts.gen_toc: 1010 | out.extend(target.line_with_break(target.get_link("Table of Contents", file="TOC", literalize=False))) 1011 | if self.opts.gen_index: 1012 | out.extend(target.line_with_break(target.get_link("Function Index", file="AlphaIndex", literalize=False))) 1013 | if self.opts.gen_topics: 1014 | out.extend(target.line_with_break(target.get_link("Topics Index", file="Topics", literalize=False))) 1015 | if self.opts.gen_glossary: 1016 | out.extend(target.line_with_break(target.get_link("Glossary", file="Glossary", literalize=False))) 1017 | if self.opts.gen_cheat: 1018 | out.extend(target.line_with_break(target.get_link("Cheat Sheet", file="CheatSheet", literalize=False))) 1019 | if self.opts.sidebar_middle: 1020 | out.extend(self.opts.sidebar_middle) 1021 | out.extend(target.paragraph()) 1022 | out.extend(target.header("List of Files:", lev=target.SUBSECTION)) 1023 | for group in groups: 1024 | out.extend(target.block_header(group if group else "Miscellaneous")) 1025 | out.extend(target.bullet_list_start()) 1026 | for fnum, fblock in enumerate(prifiles): 1027 | if fblock.group != group: 1028 | continue 1029 | file = fblock.subtitle 1030 | link = target.get_link(file, file=file, literalize=False) 1031 | for mark, note, origin in fblock.footnotes: 1032 | try: 1033 | if mark not in footmarks: 1034 | footmarks.append(mark) 1035 | if mark not in footnotes: 1036 | footnotes[mark] = note 1037 | elif note != footnotes[mark]: 1038 | raise DocsGenException("FileFootnotes", 'Footnote "{}" conflicts with previous definition "{}", while declaring block:'.format(note, footnotes[mark])) 1039 | except DocsGenException as e: 1040 | errorlog.add_entry(origin.file, origin.line, str(e), ErrorLog.FAIL) 1041 | tags = {tag: text for tag, text, origin in fblock.footnotes} 1042 | marks = target.mouseover_tags(tags, "#footnotes") 1043 | out.extend(target.bullet_list_item("{}{}".format(link, marks))) 1044 | out.extend(target.bullet_list_end()) 1045 | if footmarks: 1046 | out.append("") 1047 | out.extend(target.header("Footnotes:", lev=target.SUBSECTION)) 1048 | for mark in footmarks: 1049 | out.append("{} = {} ".format(mark, note)) 1050 | if self.opts.sidebar_footer: 1051 | out.extend(self.opts.sidebar_footer) 1052 | 1053 | out = target.postprocess(out) 1054 | outfile = os.path.join(target.docs_dir, self.SIDEBARFILE) 1055 | if not self.quiet: 1056 | print("Writing {}...".format(outfile)) 1057 | with open(outfile, "w") as f: 1058 | for line in out: 1059 | f.write(line + "\n") 1060 | 1061 | 1062 | 1063 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 1064 | -------------------------------------------------------------------------------- /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/target_githubwiki.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import re 4 | 5 | from .target_wiki import Target_Wiki 6 | 7 | 8 | class Target_GitHubWiki(Target_Wiki): 9 | def __init__(self, project_name=None, docs_dir="docs"): 10 | super().__init__(project_name=project_name, docs_dir=docs_dir) 11 | 12 | def image_block(self, item_name, title, subtitle="", code=[], code_below=False, rel_url=None, width='', height=''): 13 | out = [] 14 | out.extend(self.block_header(title, subtitle, escsub=False)) 15 | if rel_url: 16 | out.extend(self.image(item_name, title, rel_url, width=width, height=height)) 17 | if code_below: 18 | out.extend(self.markdown_block(['
'])) 19 | out.extend(self.code_block(code)) 20 | if not code_below: 21 | out.extend(self.markdown_block(['

'])) 22 | return out 23 | 24 | def image(self, item_name, img_type="", rel_url="", height='', width=''): 25 | width = ' width="{}"'.format(width) if width else '' 26 | height = ' height="{}"'.format(height) if width else '' 27 | return [ 28 | '{0} {1}'.format( 29 | self.escape_entities(item_name), 30 | self.escape_entities(img_type), 31 | rel_url, width, height 32 | ), 33 | "" 34 | ] 35 | 36 | def code_block(self, code): 37 | out = [] 38 | if code: 39 | out.extend(self.indent_lines(code)) 40 | out.append("") 41 | return out 42 | 43 | 44 | 45 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 46 | -------------------------------------------------------------------------------- /openscad_docsgen/target_wiki.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import re 4 | 5 | 6 | class Target_Wiki(object): 7 | FILE = 1 8 | SECTION = 2 9 | SUBSECTION = 2 10 | ITEM = 3 11 | def __init__(self, project_name=None, docs_dir="docs"): 12 | self.docs_dir = docs_dir 13 | self.project_name = project_name 14 | 15 | def get_suffix(self): 16 | return ".md" 17 | 18 | def postprocess(self, lines): 19 | return lines 20 | 21 | def escape_entities(self, txt): 22 | """ 23 | Escapes markdown symbols for underscores, ampersands, less-than and 24 | greater-than symbols. 25 | """ 26 | out = "" 27 | quotpat = re.compile(r'([^`]*)(`[^`]*`)(.*$)') 28 | while txt: 29 | m = quotpat.match(txt) 30 | unquot = m.group(1) if m else txt 31 | literal = m.group(2) if m else "" 32 | txt = m.group(3) if m else "" 33 | unquot = unquot.replace(r'_', r'\_') 34 | unquot = unquot.replace(r'&', r'&') 35 | unquot = unquot.replace(r'<', r'<') 36 | unquot = unquot.replace(r'>', r'>') 37 | out += unquot + literal 38 | return out 39 | 40 | def bold(self, txt): 41 | return "**{}**".format(txt) 42 | 43 | def italics(self, txt): 44 | return "*{}*".format(txt) 45 | 46 | def line_with_break(self, line): 47 | if isinstance(line,list): 48 | line[-1] += " " 49 | return line 50 | return [line + " "] 51 | 52 | def quote(self, lines=[]): 53 | if isinstance(lines,list): 54 | return [">" + line for line in lines] 55 | return [">" + lines] 56 | 57 | def paragraph(self, lines=[]): 58 | lines.append("") 59 | return lines 60 | 61 | def mouseover_tags(self, tags, file=None, htag="sup", wrap="{}"): 62 | if not file: 63 | fmt = ' <{htag} title="{text}">{abbr}' 64 | elif '#' in file: 65 | fmt = ' <{htag} title="{text}">[{abbr}]({link})' 66 | else: 67 | fmt = ' <{htag} title="{text}">[{abbr}]({link}#{linktag})' 68 | out = "".join( 69 | fmt.format( 70 | htag=htag, 71 | abbr=wrap.format(tag), 72 | text=text, 73 | link=file, 74 | linktag=self.header_link(tag) 75 | ) 76 | for tag, text in tags.items() 77 | ) 78 | return out 79 | 80 | def header_link(self, name): 81 | """ 82 | Generates markdown link for a header. 83 | """ 84 | refpat = re.compile("[^a-z0-9_ -]") 85 | return refpat.sub("", name.lower()).replace(" ", "-") 86 | 87 | def indent_lines(self, lines): 88 | return [" "*4 + line for line in lines] 89 | 90 | def get_link(self, label, anchor="", file="", literalize=True, html=False): 91 | if literalize: 92 | label = "`{0}`".format(label) 93 | else: 94 | label = self.escape_entities(label) 95 | if anchor: 96 | anchor = "#" + anchor 97 | if html: 98 | return '{}'.format(file, anchor, label) 99 | return "[{0}]({1}{2})".format(label, file, anchor) 100 | 101 | def code_span(self, txt): 102 | return "{}".format(txt) 103 | 104 | def horizontal_rule(self): 105 | return [ "---", "" ] 106 | 107 | def header(self, txt, lev=1, esc=True): 108 | return [ 109 | "{} {}".format( 110 | "#" * lev, 111 | self.escape_entities(txt) if esc else txt 112 | ), 113 | "" 114 | ] 115 | 116 | def block_header(self, title, subtitle="", escsub=True): 117 | return [ 118 | "**{}:** {}".format( 119 | self.escape_entities(title), 120 | self.escape_entities(subtitle) if escsub else subtitle 121 | ), 122 | "" 123 | ] 124 | 125 | def markdown_block(self, text=[]): 126 | out = text 127 | out.append("") 128 | return out 129 | 130 | def image_block(self, item_name, title, subtitle="", code=[], code_below=False, rel_url=None, **kwargs): 131 | out = [] 132 | out.extend(self.block_header(title, subtitle)) 133 | if not code_below: 134 | out.extend(self.code_block(code)) 135 | if rel_url: 136 | out.extend(self.image(item_name, title, rel_url)) 137 | if code_below: 138 | out.extend(self.code_block(code)) 139 | return out 140 | 141 | def image(self, item_name, img_type="", rel_url="", **kwargs): 142 | return [ 143 | '![{0} {1}]({2} "{0} {1}")'.format( 144 | self.escape_entities(item_name), 145 | self.escape_entities(img_type), 146 | rel_url 147 | ), 148 | "" 149 | ] 150 | 151 | def code_block(self, code): 152 | out = [] 153 | if code: 154 | out.append("``` {.C linenos=True}") 155 | out.extend(code) 156 | out.append("```") 157 | out.append("") 158 | return out 159 | 160 | def bullet_list_start(self): 161 | return [] 162 | 163 | def bullet_list_item(self, item): 164 | out = ["- {}".format(item)] 165 | return out 166 | 167 | def bullet_list_end(self): 168 | return [""] 169 | 170 | def bullet_list(self, items): 171 | out = self.bullet_list_start() 172 | for item in items: 173 | out.extend(self.bullet_list_item(item)) 174 | out.extend(self.bullet_list_end()) 175 | return out 176 | 177 | def numbered_list_start(self): 178 | return [] 179 | 180 | def numbered_list_item(self, num, item): 181 | out = [ 182 | "{}. {}".format(num, item) 183 | ] 184 | return out 185 | 186 | def numbered_list_end(self): 187 | return [""] 188 | 189 | def numbered_list(self, items): 190 | out = self.numbered_list_start() 191 | for num, item in enumerate(items): 192 | out.extend(self.numbered_list_item(num+1, item)) 193 | out.extend(self.numbered_list_end()) 194 | return out 195 | 196 | def table(self, headers, rows): 197 | out = [] 198 | hcells = [] 199 | lcells = [] 200 | for hdr in headers: 201 | if hdr.startswith("^"): 202 | hdr = hdr.lstrip("^") 203 | hcells.append(hdr) 204 | lcells.append("-"*min(20,len(hdr))) 205 | out.append(" | ".join(hcells)) 206 | out.append(" | ".join(lcells)) 207 | for row in rows: 208 | fcells = [] 209 | for i, cell in enumerate(row): 210 | hdr = headers[i] 211 | if hdr.startswith("^"): 212 | cell = " / ".join( 213 | "{:20s}".format("`{}`".format(x.strip())) 214 | for x in cell.split("/") 215 | ) 216 | fcells.append(cell) 217 | out.append( " | ".join(fcells) ) 218 | out.append("") 219 | return out 220 | 221 | 222 | 223 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 224 | -------------------------------------------------------------------------------- /openscad_docsgen/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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "openscad_docsgen" 7 | version = "2.0.48" 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 = "MIT" 17 | license-files = ["LICENSE"] 18 | requires-python = ">=3.7" 19 | classifiers = [ 20 | "Development Status :: 5 - Production/Stable", 21 | "Environment :: Console", 22 | "Intended Audience :: Developers", 23 | "Intended Audience :: Manufacturing", 24 | "Operating System :: MacOS :: MacOS X", 25 | "Operating System :: Microsoft :: Windows", 26 | "Operating System :: POSIX", 27 | "Programming Language :: Python :: 3", 28 | "Topic :: Artistic Software", 29 | "Topic :: Multimedia :: Graphics :: 3D Modeling", 30 | "Topic :: Multimedia :: Graphics :: 3D Rendering", 31 | "Topic :: Software Development :: Libraries", 32 | "Topic :: Software Development :: Libraries :: Python Modules", 33 | ] 34 | keywords = ["openscad", "documentation generation", "docs generation", "docsgen"] 35 | dependencies = [ 36 | "pillow>=10.3.0", 37 | "PyYAML>=6.0", 38 | "scipy>=1.15.3", 39 | "openscad_runner>=1.1.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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import setuptools 4 | 5 | if __name__ == "__main__": 6 | setuptools.setup() 7 | 8 | --------------------------------------------------------------------------------