├── LICENSE ├── README.md ├── doc └── sample.tcl ├── ruff_logo.png └── src ├── assets ├── ruff-index-min.js ├── ruff-index.js ├── ruff-logo.png ├── ruff-md.css ├── ruff-min.css ├── ruff-min.js ├── ruff.css └── ruff.js ├── diagram.tcl ├── doctools_formatter.tcl ├── formatter.tcl ├── formatter_html.tcl ├── formatter_markdown.tcl ├── formatter_nroff.tcl ├── makedist.tcl ├── makedocs.tcl ├── msgs └── de.msg ├── pkgIndex.tcl ├── ruff-yui.css └── ruff.tcl /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, apnadkarni 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruff! documentation generator 2 | 3 | Ruff! (Runtime function formatter) is a documentation generation system 4 | for programs written in the Tcl programming language. Ruff! uses runtime 5 | introspection in conjunction with comment analysis to generate reference 6 | documentation for Tcl programs with minimal effort on the programmer's part. 7 | 8 | ## Why Ruff! 9 | 10 | Ruff! produces documentation that not only requires less duplication 11 | of effort from the programmer, but is also more complete, more 12 | accurate and more maintainable. 13 | 14 | * Comments in source code do not have to be reproduced for documentation 15 | purposes. 16 | 17 | * Ruff! requires minimal markup in the comments making it lightweight 18 | as well as reducing clutter. 19 | 20 | * Supports inline formatting using Markdown syntax. 21 | 22 | * Embedded diagrams in multiple formats 23 | 24 | * Program elements like command arguments, defaults and 25 | class relationships like inheritance are automatically derived. 26 | 27 | * Maintenance is less of a burden as documentation is automatically 28 | updated with source modification such as changes to defaults, addition of 29 | mix-ins etc. 30 | 31 | On the output side, 32 | 33 | * Ruff! supports multiple formats (currently HTML, Markdown and nroff). 34 | 35 | * Generated documentation can optionally be split across multiple pages. 36 | 37 | * Hyperlinks between program elements, and optionally source code, 38 | make navigation easy and efficient. 39 | 40 | * A table of contents and optional searchable index permits quick 41 | location of command and class documentation. 42 | 43 | * Class relationships are extracted 44 | and the full API for a class, with inherited and mixed-in methods, is 45 | flattened and summarized. 46 | 47 | * HTML output supports user-selectable themes. 48 | 49 | See https://ruff.magicsplat.com for features and reference documentation. 50 | 51 | For additional examples of Ruff! generated documentation, see 52 | 53 | [iocp](https://iocp.magicsplat.com), 54 | [cffi](https://cffi.magicsplat.com), 55 | [CAWT](http://www.cawt.tcl3d.org/download/CawtReference.html), 56 | [apave](https://aplsimple.github.io/en/tcl/pave/apave.html), 57 | [baltip](https://aplsimple.github.io/en/tcl/baltip/baltip.html), 58 | [hl-tcl](https://aplsimple.github.io/en/tcl/hl_tcl/hl_tcl.html), 59 | [tcl-promise](https://tcl-promise.magicsplat.com), 60 | [tomato](https://htmlpreview.github.io/?https://raw.githubusercontent.com/nico-robert/tomato/master/documentation/tomato.html), 61 | [obex](https://tcl-obex.magicsplat.com), 62 | [Woof!](http://woof.sourceforge.net/woof-ug-0.5/html/_woof/woof_manual.html) 63 | and 64 | [tcl-vix](https://tcl-vix.magicsplat.com/). 65 | 66 | 67 | ## Release notes for 2.5 68 | 69 | * Better documentation of TclOO properties to include custom 70 | setter/getter descriptions 71 | * Bug fixes in nroff output. 72 | 73 | ## Release notes for 2.4 74 | 75 | * Support for TclOO properties in Tcl 9 76 | * Support for language specifier in fenced blocks 77 | 78 | ## Release notes for 2.3 79 | 80 | * Collapsible details section for procedure description option when 81 | `-compact 1` is specified. Note generated output with `-compact 1` has changed. 82 | * Bug fix. Ensure diagrams fit in page width 83 | * Bug fix. Index page tooltip synopsis visibility in dark themes. 84 | 85 | ## Release notes for 2.2 86 | 87 | * Support for embedded text formatted diagrams (ditaa, PlantUML etc.) 88 | * Alignment and linkable numbered captions for fenced blocks and diagrams. 89 | * Fixed minor display artifacts. 90 | 91 | ## Release notes for 2.1 92 | 93 | This release mainly has cosmetic changes in presentation. 94 | 95 | * Ensemble command includes table to subcommands. 96 | * Show command synopsis in navigation pane tooltip. 97 | * Tweaks to themes and navigation. 98 | * Bug fix: broken link to index page. 99 | 100 | ## Release notes for 2.0 101 | 102 | * Added Nroff formatter for Unix manpages. 103 | * Added themes with end-user selection. 104 | * Added end-user control for positioning navigation pane. 105 | * Classes defined with metaclasses are recognized. 106 | * Proc and method synopsis overrides, for example 107 | to distinguish invocation options. 108 | * Web assets are linked by default (option `-linkassets`) instead 109 | of being embedded. 110 | * **Incompatibility:** Generated HTML and CSS templates have changed 111 | and require modern browsers (no Internet Explorer support). 112 | * **Incompatibility:** The `-stylesheets` option is not supported. 113 | * **Incompatibility:** The `-navigation` option only takes `scrolled` 114 | `sticky` as valid values. 115 | * **Incompatibility:** The `-output` option is not supported. Use 116 | `-outfile` and `-outdir` instead. 117 | * **Incompatibility:** Output file names use hyphen as a separator 118 | instead of underscore. 119 | * [Bug fixes](https://github.com/apnadkarni/ruff/issues?q=is%3Aissue+is%3Aclosed+milestone%3Av2.0+label%3Abug) 120 | 121 | 122 | 123 | See https://ruff.magicsplat.com/ruff.html#History. 124 | 125 | NOTE 2.0 HAS SEVERAL INCOMPATIBILITIES VIS-A-VIS 1.x IN OPTIONS AND GENERATED 126 | OUTPUT FORMAT. See above link for details. 127 | -------------------------------------------------------------------------------- /doc/sample.tcl: -------------------------------------------------------------------------------- 1 | # This file contains a sample that demonstrates Ruff!'s documentation 2 | # features. It also serves as test input. 3 | 4 | namespace eval ruff::sample { 5 | variable _ruff_preamble { 6 | ## Introduction 7 | 8 | The code in this namespace illustrates the various documentation 9 | features in [Ruff!](ruff.html). The corresponding source is 10 | [here](sample.tcl) or click the *Show source* link below each procedure 11 | or method documentation to see the source from which the 12 | documentation was generated. See the main [::ruff] documentation 13 | for a full description. 14 | 15 | The documentation (such as this section) not specific to a *procedure* 16 | or *method* is placed in the variable `_ruff_preamble` within each 17 | namespace. 18 | 19 | ## Formatting 20 | 21 | The formatting elements described below may appear both within 22 | `_ruff_preamble` content as well as proc and method comments. 23 | 24 | ### Lists 25 | 26 | This is an **unnumbered list**. 27 | ```` 28 | * First item 29 | * Second 30 | item 31 | across multiple lines 32 | * Third item 33 | ```` 34 | This is displayed as 35 | 36 | * First item 37 | * Second 38 | item 39 | across multiple lines 40 | * Third item 41 | 42 | 43 | This is a **definition list**. 44 | ```` 45 | itema - Definition of item A 46 | itemb - Definition of item B 47 | across multiple lines. 48 | ```` 49 | 50 | Definition lists are displayed in an output-specific format. 51 | 52 | itema - Definition of item A 53 | itemb - Definition of item B 54 | across multiple lines. 55 | 56 | ### Inline formatting 57 | 58 | ```` 59 | Basic markdown inline formatting is supported as 60 | `code`, *emphasis*, **strong** and ***strong emphasis***. 61 | ```` 62 | Basic markdown inline formatting is supported as 63 | `code`, *emphasis*, **strong** and ***strong emphasis***. 64 | 65 | ### Links 66 | 67 | ```` 68 | Links may be references to program elements, e.g. [Derived], to 69 | external resources, e.g. [example](https://www.example.com) or 70 | explicit, e.g. . 71 | ```` 72 | Links may be references to program elements, e.g. [Derived], to 73 | external resources, e.g. [example](https://www.example.com) or 74 | explicit, e.g. . 75 | 76 | 77 | ### Fenced blocks 78 | 79 | ```` 80 | ``` -caption "A fenced block" 81 | Lines consisting of *3* or more backquotes can be used 82 | to bracket unformatted content 83 | like 84 | this paragraph. 85 | ``` 86 | ```` 87 | 88 | ``` -caption "A fenced block" 89 | Lines consisting of *3* or more backquotes can be used 90 | to bracket unformatted content 91 | like 92 | this paragraph. 93 | ``` 94 | 95 | The `-caption` option is optional. If specified, it is shown 96 | below the output and can be linked to using the value of the option. 97 | For example `[A fenced block]` will link as [A fenced block]. 98 | 99 | In the HTML formatter, captions are automatically prefixed as *Figure 100 | 1*, *Figure 2* etc.. The numbering is across the entire document set, 101 | not per page, by design. 102 | 103 | The fence can be followed by a language specifier (any string) 104 | *without any intervening whitespace. 105 | 106 | ```` 107 | ```tcl 108 | set lang "This is Tcl code" 109 | ``` 110 | ```` 111 | 112 | ```tcl 113 | set lang "This is Tcl code" 114 | ``` 115 | 116 | Not all formatters make use of the language specifier. 117 | 118 | ### Diagrams 119 | 120 | Diagrams can be embedded in several formats such as `ditaa`, 121 | 122 | ```` 123 | ``` -caption "A ditaa diagram" diagram ditaa 124 | +--------+ +-------+ +-------+ 125 | | | --+ ditaa +--> | | 126 | | Text | +-------+ |diagram| 127 | |Document| |!magic!| | | 128 | | {d}| | | | | 129 | +---+----+ +-------+ +-------+ 130 | : ^ 131 | | Lots of work | 132 | +-------------------------+ 133 | ``` 134 | ```` 135 | 136 | ``` -caption "A ditaa diagram" diagram ditaa 137 | +--------+ +-------+ +-------+ 138 | | | --+ ditaa +--> | | 139 | | Text | +-------+ |diagram| 140 | |Document| |!magic!| | | 141 | | {d}| | | | | 142 | +---+----+ +-------+ +-------+ 143 | : ^ 144 | | Lots of work | 145 | +-------------------------+ 146 | ``` 147 | 148 | or PlantUML, 149 | 150 | ```` 151 | ``` -caption "A PlantUML diagram generated by kroki" diagram kroki plantuml 152 | @startuml 153 | Alice -> Bob: Authentication Request 154 | Bob --> Alice: Authentication Response 155 | 156 | Alice -> Bob: Another authentication Request 157 | Alice <-- Bob: Another authentication Response 158 | @enduml 159 | ``` 160 | ```` 161 | 162 | ``` -caption "A PlantUML diagram generated by kroki" diagram kroki plantuml 163 | @startuml 164 | Alice -> Bob: Authentication Request 165 | Bob --> Alice: Authentication Response 166 | 167 | Alice -> Bob: Another authentication Request 168 | Alice <-- Bob: Another authentication Response 169 | @enduml 170 | ``` 171 | 172 | and several others. You can link to a diagram using the string 173 | specified with its `-caption` option. 174 | 175 | ### Images 176 | 177 | Images can be specified using either Markdown or HTML: 178 | 179 | * ![alt img](assets/ruff-logo.png) `![alt img](assets/ruff-logo.png)` 180 | * logo `logo` 181 | * `` 182 | 183 | 184 | 185 | 186 | The remaining sections show how commands and classes are documented. 187 | Click on the *Show source* link to see the underlying source code 188 | for the procedure or method from which the documentation was generated. 189 | } 190 | 191 | namespace eval ensemble_proc { 192 | proc cmdA {} { 193 | # Implements cmdA for an ensemble procedure 194 | } 195 | 196 | proc cmdB {args} { 197 | # Implements cmdB for an ensemble procedure 198 | # paramA - the first param 199 | # paramB - the optional second param 200 | # Synopsis: paramA paramB 201 | # Synopsis: paramX 202 | 203 | } 204 | namespace export * 205 | namespace ensemble create 206 | } 207 | } 208 | 209 | proc ruff::sample::proc_without_docs {first_arg second_arg} { 210 | } 211 | 212 | proc ruff::sample::proc_full {arg {optarg AVALUE} args} { 213 | # This first line is the summary line for documentation. 214 | # arg - first parameter 215 | # optarg - an optional parameter 216 | # -switch VALUE - an optional switch 217 | # 218 | # This is the general description of the procedure 219 | # composed of multiple paragraphs. It is separated from 220 | # the parameter list above by one or more empty comments. 221 | # 222 | # This is the second paragraph. The next paragraph (in the *source* comments) 223 | # starts with the word Returns and hence will be treated 224 | # by Ruff! as describing the return value. 225 | # 226 | # Returns a value. Because it started with the **Returns** 227 | # keyword, this paragraph is treated as the return value 228 | # description no matter where it appears. 229 | # 230 | # A definition list has a similar form to the argument 231 | # list. For example, optarg may take the following values: 232 | # AVALUE - one possible value 233 | # BVALUE - another possible value 234 | # 235 | # CVALUE - a third value but note the intervening blank comment 236 | # above in the source code. 237 | # Bullet lists are indicated by a starting `-` or `*` character. 238 | # - This is a bullet list iterm 239 | # * This is also a bullet list item 240 | # 241 | # An optional *See also* section may be used to 242 | # cross-reference other program elements. Each line of this section 243 | # must be parsable as a Tcl list. 244 | # 245 | # See also: proc_without_docs [Base] 246 | # "ensemble_proc cmdA" {ensemble_proc cmdB} 247 | # See also: proc_full 248 | # See also: character_at 249 | 250 | 251 | # This paragraph will be ignored by Ruff! as it is not part 252 | # of the initial block of comments. 253 | 254 | some code 255 | 256 | #ruff 257 | # Thanks to the #ruff marker, this paragraph will be 258 | # included by Ruff! even though it is not in the initial block 259 | # of comments. This is useful for putting documentation for 260 | # a feature right next to the code implementing it. 261 | 262 | some more code. 263 | } 264 | 265 | proc ruff::sample::proc_with_custom_synopsis {args} { 266 | # This is a proc with a custom synopsis 267 | # A - arg A for first synopsis 268 | # B - arg V for first synopsis 269 | # DIFFERENT_PARAM_SIG - arg for another synopsis 270 | # A custom synopsis is useful when a command takes several 271 | # different argument structures. 272 | # Synopsis: A B 273 | # C D 274 | # Synopsis: DIFFERENT_PARAM_SIG 275 | # Synopsis: X Y args 276 | } 277 | 278 | proc ruff::sample::character_at {text {pos 0}} { 279 | # Get the character from a string. 280 | # text - Text string. 281 | # pos - Character position. 282 | # The command will treat negative values of $pos as offset from 283 | # the end of the string. 284 | # 285 | # Note the use of `Returns:` as opposed to `Returns` (i.e. with a colon) in 286 | # the source comments. See docs for the difference. 287 | # 288 | # Returns: The character at index $pos in string $text. 289 | set n [string length $text] 290 | if {[tcl::mathfunc::abs $pos] >= [string length $text]} { 291 | #ruff 292 | # An error exception is raised if $pos is not within bounds. 293 | error "Index $pos out of bounds." 294 | } 295 | if {$pos < 0} { 296 | return [string index $text end$pos] 297 | } else { 298 | return [string index $text $pos] 299 | } 300 | } 301 | 302 | oo::class create ruff::sample::Base { 303 | constructor {arg} { 304 | # Constructs the class 305 | # arg - argument to constructor 306 | # The constructor for the class should also include 307 | # general information for the class. 308 | } 309 | destructor { 310 | # Releases all resources and destroys the class 311 | } 312 | method base_method {arga argb} { 313 | # base_method is defined only in the base class 314 | # arga - first argument 315 | # argb - second argument from 316 | # 317 | # This is method m1 318 | # 319 | # This is a reference to method []. 320 | # 321 | # See also: 322 | } 323 | method property args { 324 | # Method with custom synopsis 325 | # PROPERTYNAME - name of property 326 | # VALUE - value to set for property 327 | # Synopsis: PROPERTYNAME 328 | # Synopsis: PROPERTYNAME ?VALUE? 329 | } 330 | method overridable_method {} { 331 | # This method will be overridden in the derived class 332 | # 333 | # Calls [base_method] 334 | } 335 | method {} { 336 | # An explicitly exported method looking like a html tag 337 | # 338 | # Verify it also shows in [base_method] description as well 339 | # as See also section. 340 | } 341 | forward fwd_method string range 342 | export 343 | 344 | } 345 | 346 | oo::class create ruff::sample::Mixin { 347 | method mixed_in_method {arg} { 348 | # This method will be mixed into a class. 349 | # arg - an argument to the method 350 | } 351 | } 352 | 353 | oo::class create ruff::sample::Derived { 354 | superclass ::ruff::sample::Base 355 | mixin ::ruff::sample::Mixin 356 | method overridable_method {} { 357 | # This method overrides the one defined in [Base]. 358 | } 359 | method added_method {} { 360 | # Method defined only in [Derived]. 361 | } 362 | } 363 | 364 | oo::class create ruff::sample::FunnyMethods { 365 | constructor {} { 366 | # Class for testing special characters in method names 367 | } 368 | method + {} { 369 | # Method to test regexp special chars 370 | } 371 | method * {} { 372 | # Method to test regexp special chars 373 | } 374 | method > {} { 375 | # Method to test > escaping in HTML 376 | } 377 | method < {} { 378 | # Method to test < escaping in HTML 379 | } 380 | method & {} { 381 | # Method to test & escaping in HTML 382 | } 383 | method _ {} { 384 | # Method to test underscores 385 | } 386 | export + * > < & _ 387 | 388 | } 389 | 390 | 391 | oo::class create ruff::sample::MetaClass { 392 | superclass ::oo::class 393 | } 394 | 395 | ruff::sample::MetaClass create ruff::sample::MetaClassInstance { 396 | method amethod {} { 397 | # A method of a class created from a metaclass 398 | } 399 | } 400 | 401 | if {[package vsatisfies [package require Tcl] 9]} { 402 | oo::configurable create ruff::sample::ConfigurableClass { 403 | property rprop -kind readable 404 | property wprop -kind writable 405 | property rwprop -set { 406 | # This is a special function for setting a rw property. 407 | # 408 | # Some more information about the same. 409 | } -get { 410 | # This is a special function for reading a rw property. 411 | } 412 | 413 | constructor {} { 414 | # A class with properties 415 | } 416 | } 417 | oo::configurable create ruff::sample::DerivedConfigurableClass { 418 | superclass ruff::sample::ConfigurableClass 419 | property derivedprop -get { 420 | # This is a derived property. 421 | } 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /ruff_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apnadkarni/ruff/029cf61ed2f22283e6d5ed651e100377cf7ed854/ruff_logo.png -------------------------------------------------------------------------------- /src/assets/ruff-index-min.js: -------------------------------------------------------------------------------- 1 | function myIndexInit(){var single,nvisible,urltext,a;filterElement=document.getElementById("filterText");urltext=myGetUrlParameter("lookup");if(urltext==""){urltext=myGetUrlParameter("search")}if(urltext!=""){filterElement.value=urltext;myRunFilter()}filterElement.focus()}function myGetUrlParameter(name){name=name.replace(/[\[]/,"\\[").replace(/[\]]/,"\\]");var regex=new RegExp("[\\?&]"+name+"=([^&#]*)");var results=regex.exec(location.search);return results===null?"":decodeURIComponent(results[1].replace(/\+/g," "))}var myDebounceDelay;var myUserAgent=navigator.userAgent.toUpperCase();if(myUserAgent.indexOf("MSIE")!=-1||myUserAgent.indexOf("TRIDENT")!=-1){myDebounceDelay=300}else if(myUserAgent.indexOf("EDGE")!=-1){myDebounceDelay=300}else{myDebounceDelay=100}function myDebounce(func,wait,immediate){var timeout;return function(){var context=this,args=arguments;var later=function(){timeout=null;if(!immediate)func.apply(context,args)};var callNow=immediate&&!timeout;clearTimeout(timeout);timeout=setTimeout(later,wait);if(callNow)func.apply(context,args)}}function mySetStatus(text){var status;status=document.getElementById("indexStatus");status.innerText=text}function myResetStatus(){var status;status=document.getElementById("indexStatus");status.innerText=" "}function myRunFilter(){var input,filter,filter0,ul,li,a,i,txtValue,matchSeen,firstMatch;input=document.getElementById("filterText");filter=input.value.toUpperCase();filter0=filter.charAt(0);ul=document.getElementById("indexUL");li=ul.getElementsByTagName("li");if(filter==""){myResetStatus();for(i=0;ispan{text-decoration:underline}.ruff_dyn_src{display:none}.ruff-bd .ruff_synopsis{border:var(--ruff-bd-synopsis-border);margin:0 2em 1em;padding:.5em}.ruff-bd .ruff_arg,.ruff-bd .ruff_cmd,.ruff-bd .ruff_const,.ruff-bd .ruff_synopsis,.ruff-bd code{font-family:Consolas,"Courier New",monospace}.ruff-bd .ruff_arg{font-style:italic;font-size:smaller}.ruff-bd .ruff_source_link{font-size:small}.ruff-bd .ruff_source_link a[href]{color:var(--ruff-bd-sourcelink-color);background-color:var(--ruff-bd-sourcelink-background-color);text-decoration:underline}.ruff_index{font-size:smaller}.ruff_index ul li{list-style-type:none}.ruff_index ul li a{text-decoration:none}#indexUL,#ruffNavMove{line-height:1}.ruff-bd #indexUL .ruff-tiptext,.ruff-bd #indexUL .ruff-tiptext pre,.ruff-bd #indexUL .ruff-tiptext pre .ruff_arg,.ruff-bd #indexUL .ruff-tiptext pre .ruff_cmd{background-color:var(--ruff-bd-tip-background-color);color:var(--ruff-bd-tip-color)}.ruff-figure{margin:.5em 1em}.ruff-snippet{background-color:var(--ruff-bd-code-background-color)}.ruff-left{text-align:left}.ruff-center{text-align:center}.ruff-right{text-align:right}.ruff-caption{font-style:italic;font-size:smaller;background-color:var(--ruff-bd-background-color)}.ruff-figure img{max-width:100%;height:auto}.ruff-expand>span{font-size:small}summary.ruff-expand{margin-bottom:1em} -------------------------------------------------------------------------------- /src/assets/ruff-min.js: -------------------------------------------------------------------------------- 1 | function toggleSource(id){var elem;var link;if(document.getElementById){elem=document.getElementById(id);link=document.getElementById("l_"+id)}else if(document.all){elem=eval("document.all."+id);link=eval("document.all.l_"+id)}else return false;if(elem.style.display=="block"){elem.style.display="none";link.innerHTML="Show source"}else{elem.style.display="block";link.innerHTML="Hide source"}}function ruffSetTheme(themeName){localStorage.ruff_theme=themeName;document.documentElement.className="ruff-theme-".concat(themeName)}function ruffNextTheme(){themeNames=JSON.parse(localStorage.ruff_themes);currentTheme=localStorage.ruff_theme;if(currentTheme===undefined){themeIndex=0}else{themeIndex=themeNames.indexOf(currentTheme);++themeIndex;if(themeIndex>=themeNames.length){themeIndex=0}}ruffSetTheme(themeNames[themeIndex])}function ruffSetNavSide(navSide){localStorage.ruff_nav_side=navSide;but=document.getElementById("ruffNavMove");if(navSide==="right"){gridAreas='"toparea toparea" "mainarea navarea" "botarea botarea"';gridCols="1fr minmax(200px, min-content)";but.textContent="◀";but.style.setProperty("border-left","none");but.style.setProperty("border-right-style","solid");but.style.setProperty("border-right-width","thick")}else{gridAreas='"toparea toparea" "navarea mainarea" "botarea botarea"';gridCols="minmax(200px, min-content) 1fr";but.textContent="▶";but.style.setProperty("border-right","none");but.style.setProperty("border-left-style","solid");but.style.setProperty("border-left-width","thick")}document.documentElement.style.setProperty("--ruff-grid-template-areas",gridAreas);document.documentElement.style.setProperty("--ruff-grid-template-columns",gridCols)}function ruffMoveNavPane(){if(localStorage.ruff_nav_side==="left")ruffSetNavSide("right");else ruffSetNavSide("left")}(function(){themeNames=["v1","light","dark","slate","solar","clouds","maroon"];localStorage.ruff_themes=JSON.stringify(themeNames);navSide=localStorage.ruff_nav_side;if(navSide!=="left"&&navSide!=="right")navSide="left";window.onload=init;function init(){currentTheme=localStorage.ruff_theme;if(currentTheme===undefined||themeNames.indexOf(currentTheme)<0){currentTheme="v1"}ruffSetTheme(currentTheme);navSide=localStorage.ruff_nav_side;if(navSide!=="right")navSide="left";ruffSetNavSide(navSide)}})(); -------------------------------------------------------------------------------- /src/assets/ruff.css: -------------------------------------------------------------------------------- 1 | /* To minimize: 2 | csso -i ruff.css -o ruff-min.cscsso -i ruff.css -o ruff-min.css 3 | */ 4 | :root { 5 | --ruff-grid-template-rows: min-content 1fr; 6 | /* TBD - maybe use fit-content(200px) instead of minmax(...) */ 7 | --ruff-grid-template-columns: minmax(200px, min-content) 1fr; 8 | --ruff-grid-template-areas: "toparea toparea" "navarea mainarea" "botarea botarea"; 9 | --ruff-tip-z-index: 10; 10 | 11 | /* This is dynamically set in the generated body to set level of indentation for h2... */ 12 | --ruff-nav-toc-offset: 0em; 13 | 14 | /* 15 | * This padding is to ensure auto scrollbar does not change centering pages 16 | * between pages that need a scrollbar and those that do not. 17 | * See https://stackoverflow.com/questions/1417934/how-to-prevent-scrollbar-from-repositioning-web-page. 18 | */ 19 | padding-left: calc(100vw - 100%); 20 | 21 | /* Default theme button gradient */ 22 | --ruff-theme-gradient: linear-gradient(90deg, lightblue, coral, lightgreen); 23 | } 24 | 25 | /* Color theming support from https://dev.to/ananyaneogi/create-a-dark-light-mode-switch-with-css-variables-34l8 */ 26 | 27 | .ruff-theme-light { 28 | --ruff-color: #444; 29 | --ruff-background-color: whitesmoke; 30 | --ruff-minor-color: #888; 31 | 32 | --ruff-layout-background-color: #fefefe; 33 | 34 | --ruff-hd-color: #666; 35 | --ruff-hd-background-color: inherit; 36 | --ruff-hd-font: large bold; 37 | 38 | --ruff-nav-background-color: #FFF5EA; 39 | --ruff-nav-color: #666; 40 | --ruff-nav-tip-color: var(--ruff-nav-background-color); 41 | --ruff-nav-tip-background-color: var(--ruff-nav-color); 42 | --ruff-nav-highlight-color: var(--ruff-nav-background-color); 43 | --ruff-nav-highlight-color: var(--ruff-background-color); 44 | --ruff-nav-highlight-background-color: coral; 45 | 46 | --ruff-bd-background-color: var(--ruff-layout-background-color); 47 | --ruff-bd-color: var(--ruff-color); 48 | --ruff-bd-minor-color: var(--ruff-minor-color); 49 | --ruff-bd-table-border: #808080; 50 | --ruff-bd-code-color: var(--ruff-bd-color); 51 | --ruff-bd-code-background-color: whitesmoke; 52 | --ruff-bd-h-background-color: inherit; 53 | --ruff-bd-h-color: #968C83; 54 | /* H1 - special treatment to match navigation pane */ 55 | --ruff-bd-h1-color: #666; 56 | --ruff-bd-h1-background-color: var(--ruff-nav-background-color); 57 | --ruff-bd-a-color: blue; 58 | --ruff-bd-sourcelink-color: var(--ruff-bd-a-color); 59 | --ruff-bd-sourcelink-background-color: var(--ruff-bd-background-color); 60 | --ruff-bd-synopsis-border: none; 61 | --ruff-bd-tip-color: inherit; 62 | --ruff-bd-tip-background-color: var(--ruff-nav-background-color); 63 | 64 | --ruff-ft-color: var(--ruff-bd-minor-color); 65 | --ruff-ft-minor-color: var(--ruff-ft-color); 66 | --ruff-ft-background-color: var(--ruff-bd-background-color); 67 | } 68 | 69 | .ruff-theme-clouds { 70 | --ruff-color: #111; 71 | --ruff-background-color: whitesmoke; 72 | --ruff-minor-color: #888; 73 | 74 | --ruff-layout-background-color: azure; 75 | 76 | --ruff-hd-color: #666; 77 | --ruff-hd-background-color: inherit; 78 | --ruff-hd-font: large bold; 79 | 80 | --ruff-nav-background-color: lightblue; 81 | --ruff-nav-color: #212121; 82 | --ruff-nav-tip-color: var(--ruff-nav-background-color); 83 | --ruff-nav-tip-background-color: var(--ruff-nav-color); 84 | --ruff-nav-highlight-color: var(--ruff-nav-background-color); 85 | --ruff-nav-highlight-color: var(--ruff-background-color); 86 | --ruff-nav-highlight-background-color: #14a7ff; 87 | 88 | --ruff-bd-background-color: var(--ruff-layout-background-color); 89 | --ruff-bd-color: var(--ruff-color); 90 | --ruff-bd-minor-color: var(--ruff-minor-color); 91 | --ruff-bd-table-border: #808080; 92 | --ruff-bd-code-color: var(--ruff-bd-color); 93 | --ruff-bd-code-background-color: #cfebf7; 94 | --ruff-bd-code-background-color: #cffcff; 95 | --ruff-bd-h-background-color: inherit; 96 | --ruff-bd-h-color: #968C83; 97 | /* H1 - special treatment to match navigation pane */ 98 | --ruff-bd-h1-color: #666; 99 | --ruff-bd-h1-background-color: var(--ruff-nav-background-color); 100 | --ruff-bd-a-color: blue; 101 | --ruff-bd-sourcelink-color: var(--ruff-bd-a-color); 102 | --ruff-bd-sourcelink-background-color: var(--ruff-bd-background-color); 103 | --ruff-bd-synopsis-border: none; 104 | --ruff-bd-tip-color: inherit; 105 | --ruff-bd-tip-background-color: var(--ruff-nav-background-color); 106 | 107 | --ruff-ft-color: var(--ruff-bd-minor-color); 108 | --ruff-ft-minor-color: var(--ruff-ft-color); 109 | --ruff-ft-background-color: var(--ruff-bd-background-color); 110 | } 111 | 112 | .ruff-theme-dark { 113 | --ruff-color: #ddd; 114 | --ruff-background-color: #27242c; 115 | --ruff-minor-color: #aaa; 116 | 117 | --ruff-layout-background-color: #212121; 118 | 119 | --ruff-hd-color: inherit; 120 | --ruff-hd-background-color: inherit; 121 | --ruff-hd-font: large bold; 122 | 123 | --ruff-nav-background-color: #22272e; 124 | --ruff-nav-color: var(--ruff-color); 125 | --ruff-nav-tip-color: var(--ruff-nav-background-color); 126 | --ruff-nav-tip-background-color: var(--ruff-nav-color); 127 | --ruff-nav-highlight-color: #e6eff5; 128 | --ruff-nav-highlight-background-color: #1164a3; 129 | 130 | --ruff-bd-background-color: var(--ruff-layout-background-color); 131 | --ruff-bd-color: var(--ruff-color); 132 | --ruff-bd-minor-color: var(--ruff-minor-color); 133 | --ruff-bd-table-border: #808080; 134 | --ruff-bd-code-color: inherit; 135 | --ruff-bd-code-background-color: var(--ruff-nav-background-color); 136 | --ruff-bd-h-background-color: inherit; 137 | --ruff-bd-h-color: lightblue; 138 | /* H1 - special treatment to match navigation pane */ 139 | --ruff-bd-h1-color: var(--ruff-nav-color); 140 | --ruff-bd-h1-background-color: var(--ruff-nav-background-color); 141 | --ruff-bd-a-color: #489bf5; 142 | --ruff-bd-sourcelink-color: var(--ruff-bd-a-color); 143 | --ruff-bd-sourcelink-background-color: var(--ruff-bd-background-color); 144 | --ruff-bd-synopsis-border: none; 145 | --ruff-bd-tip-color: var(--ruff-bd-background-color); 146 | --ruff-bd-tip-background-color: var(--ruff-bd-color); 147 | 148 | --ruff-ft-color: var(--ruff-bd-minor-color); 149 | --ruff-ft-minor-color: var(--ruff-ft-color); 150 | --ruff-ft-background-color: var(--ruff-bd-background-color); 151 | } 152 | 153 | .ruff-theme-solar { 154 | --ruff-color: whitesmoke; 155 | --ruff-background-color: #011; 156 | --ruff-minor-color: #aaa; 157 | 158 | --ruff-layout-background-color: #002b35; 159 | 160 | --ruff-hd-color: inherit; 161 | --ruff-hd-background-color: inherit; 162 | --ruff-hd-font: large bold; 163 | 164 | --ruff-nav-background-color: #003641; 165 | --ruff-nav-color: var(--ruff-color); 166 | --ruff-nav-tip-color: var(--ruff-nav-background-color); 167 | --ruff-nav-tip-background-color: var(--ruff-nav-color); 168 | --ruff-nav-highlight-color: var(--ruff-nav-background-color); 169 | --ruff-nav-highlight-background-color: var(--ruff-nav-color); 170 | 171 | --ruff-bd-background-color: var(--ruff-layout-background-color); 172 | --ruff-bd-color: var(--ruff-color); 173 | --ruff-bd-minor-color: var(--ruff-minor-color); 174 | --ruff-bd-table-border: #808080; 175 | --ruff-bd-code-color: inherit; 176 | --ruff-bd-code-background-color: #003641; 177 | --ruff-bd-h-background-color: inherit; 178 | --ruff-bd-h-color: cornsilk; 179 | /* H1 - special treatment to match navigation pane */ 180 | --ruff-bd-h1-color: var(--ruff-nav-color); 181 | --ruff-bd-h1-background-color: var(--ruff-nav-background-color); 182 | --ruff-bd-a-color: palegreen; 183 | --ruff-bd-sourcelink-color: var(--ruff-bd-a-color); 184 | --ruff-bd-sourcelink-background-color: var(--ruff-bd-background-color); 185 | --ruff-bd-synopsis-border: none; 186 | --ruff-bd-tip-color: var(--ruff-bd-color); 187 | --ruff-bd-tip-background-color: var(--ruff-nav-background-color); 188 | 189 | --ruff-ft-color: var(--ruff-bd-minor-color); 190 | --ruff-ft-minor-color: var(--ruff-ft-color); 191 | --ruff-ft-background-color: var(--ruff-bd-background-color); 192 | } 193 | 194 | .ruff-theme-slate { 195 | --ruff-color: #ccc; 196 | --ruff-background-color: #829ab1; 197 | --ruff-minor-color: #aaa; 198 | 199 | --ruff-layout-background-color: #181a26; 200 | --ruff-layout-background-color: #1a202c; 201 | 202 | --ruff-hd-color: inherit; 203 | --ruff-hd-background-color: inherit; 204 | --ruff-hd-font: large bold; 205 | 206 | --ruff-nav-background-color: rgb(45,55,72); 207 | --ruff-nav-color: var(--ruff-color); 208 | --ruff-nav-tip-color: var(--ruff-nav-background-color); 209 | --ruff-nav-tip-background-color: var(--ruff-nav-color); 210 | --ruff-nav-highlight-color: var(--ruff-nav-background-color); 211 | --ruff-nav-highlight-background-color: var(--ruff-nav-color); 212 | 213 | --ruff-bd-background-color: var(--ruff-layout-background-color); 214 | --ruff-bd-color: var(--ruff-color); 215 | --ruff-bd-minor-color: var(--ruff-minor-color); 216 | --ruff-bd-table-border: #808080; 217 | --ruff-bd-code-color: #ecdbba; 218 | --ruff-bd-code-background-color: var(--ruff-nav-background-color); 219 | --ruff-bd-h-background-color: inherit; 220 | --ruff-bd-h-color: lightsteelblue; 221 | /* H1 - special treatment to match navigation pane */ 222 | --ruff-bd-h1-color: var(--ruff-nav-color); 223 | --ruff-bd-h1-background-color: var(--ruff-nav-background-color); 224 | --ruff-bd-a-color: lightskyblue; 225 | --ruff-bd-sourcelink-color: var(--ruff-bd-a-color); 226 | --ruff-bd-sourcelink-background-color: var(--ruff-bd-background-color); 227 | --ruff-bd-synopsis-border: none; 228 | --ruff-bd-tip-color: var(--ruff-bd-color); 229 | --ruff-bd-tip-background-color: var(--ruff-nav-background-color); 230 | 231 | --ruff-ft-color: var(--ruff-bd-minor-color); 232 | --ruff-ft-minor-color: var(--ruff-ft-color); 233 | --ruff-ft-background-color: var(--ruff-bd-background-color); 234 | } 235 | 236 | /* The original ruff theme */ 237 | .ruff-theme-v1 { 238 | --ruff-color: #121212; 239 | --ruff-background-color: white; 240 | --ruff-minor-color: #888; 241 | 242 | --ruff-layout-background-color: white; 243 | 244 | --ruff-hd-color: inherit; 245 | --ruff-hd-background-color: inherit; 246 | --ruff-hd-font: large bold; 247 | 248 | --ruff-nav-background-color: #006666; 249 | --ruff-nav-color: white; 250 | --ruff-nav-tip-color: var(--ruff-nav-background-color); 251 | --ruff-nav-tip-background-color: whitesmoke; 252 | --ruff-nav-highlight-color: var(--ruff-nav-background-color); 253 | --ruff-nav-highlight-background-color: var(--ruff-nav-color); 254 | 255 | --ruff-bd-background-color: var(--ruff-layout-background-color); 256 | --ruff-bd-color: var(--ruff-color); 257 | --ruff-bd-minor-color: var(--ruff-minor-color); 258 | --ruff-bd-table-border: #808080; 259 | --ruff-bd-code-color: var(--ruff-bd-color); 260 | --ruff-bd-code-background-color: whitesmoke; 261 | --ruff-bd-h-background-color: inherit; 262 | --ruff-bd-h-color: var(--ruff-nav-background-color); 263 | /* H1 - special treatment to match navigation pane */ 264 | --ruff-bd-h1-color: var(--ruff-nav-color); 265 | --ruff-bd-h1-background-color: var(--ruff-nav-background-color); 266 | --ruff-bd-a-color: blue; 267 | --ruff-bd-sourcelink-color: var(--ruff-bd-a-color); 268 | --ruff-bd-sourcelink-background-color: var(--ruff-bd-background-color); 269 | --ruff-bd-synopsis-border: none; 270 | --ruff-bd-tip-color: inherit; 271 | --ruff-bd-tip-background-color: whitesmoke; 272 | 273 | --ruff-ft-color: var(--ruff-bd-minor-color); 274 | --ruff-ft-minor-color: var(--ruff-ft-color); 275 | --ruff-ft-background-color: var(--ruff-bd-background-color); 276 | } 277 | 278 | .ruff-theme-maroon { 279 | --ruff-color: #844; 280 | --ruff-background-color: whitesmoke; 281 | --ruff-minor-color: #888; 282 | 283 | --ruff-layout-background-color: white; 284 | 285 | --ruff-hd-color: inherit; 286 | --ruff-hd-background-color: inherit; 287 | --ruff-hd-font: large bold; 288 | 289 | --ruff-nav-background-color: maroon; 290 | --ruff-nav-color: white; 291 | --ruff-nav-tip-color: var(--ruff-nav-background-color); 292 | --ruff-nav-tip-background-color: whitesmoke; 293 | --ruff-nav-highlight-color: var(--ruff-nav-background-color); 294 | --ruff-nav-highlight-background-color: var(--ruff-nav-color); 295 | 296 | --ruff-bd-background-color: var(--ruff-layout-background-color); 297 | --ruff-bd-color: #212121; 298 | --ruff-bd-minor-color: var(--ruff-minor-color); 299 | --ruff-bd-table-border: #808080; 300 | --ruff-bd-code-color: var(--ruff-bd-color); 301 | --ruff-bd-code-background-color: #fff0f0; 302 | --ruff-bd-h-background-color: inherit; 303 | --ruff-bd-h-color: var(--ruff-nav-background-color); 304 | /* H1 - special treatment to match navigation pane */ 305 | --ruff-bd-h1-color: var(--ruff-nav-color); 306 | --ruff-bd-h1-background-color: var(--ruff-nav-background-color); 307 | --ruff-bd-a-color: #44f; 308 | --ruff-bd-sourcelink-color: var(--ruff-bd-a-color); 309 | --ruff-bd-sourcelink-background-color: var(--ruff-bd-background-color); 310 | --ruff-bd-synopsis-border: none; 311 | --ruff-bd-tip-color: inherit; 312 | --ruff-bd-tip-background-color: whitesmoke; 313 | 314 | --ruff-ft-color: var(--ruff-bd-minor-color); 315 | --ruff-ft-minor-color: var(--ruff-ft-color); 316 | --ruff-ft-background-color: var(--ruff-bd-background-color); 317 | } 318 | 319 | 320 | #ruffButtonBar { 321 | float: right; 322 | } 323 | 324 | #ruffToggleTheme { 325 | background-image: var(--ruff-theme-gradient); 326 | transition: 0.25s; 327 | background-size: 200% auto; 328 | border: none; 329 | width: 20px; 330 | height: 20px; 331 | cursor: pointer; 332 | vertical-align: text-top; 333 | } 334 | 335 | #ruffToggleTheme:hover { 336 | background-position: right center; /* change the direction of the change here */ 337 | vertical-align: text-top; 338 | } 339 | 340 | #ruffNavMove { 341 | color: lightblue; 342 | border-color: lightblue; 343 | background-color: var(--ruff-bd-background-color); 344 | height: 20px; 345 | text-align: center; 346 | border: none; 347 | cursor: pointer; 348 | vertical-align: text-top; /* Move button up */ 349 | line-height: 1; /* To vertically center text within button */ 350 | } 351 | 352 | 353 | *, *::before, *::after { 354 | box-sizing:border-box; 355 | } 356 | 357 | body { 358 | color: var(--ruff-color); 359 | margin: 0; 360 | background-color: var(--ruff-background-color); 361 | } 362 | 363 | a, a:visited { 364 | color: inherit; 365 | background-color: inherit; 366 | } 367 | 368 | .ruff-layout { 369 | display: grid; 370 | grid-template-rows: var(--ruff-grid-template-rows); 371 | grid-template-columns: var(--ruff-grid-template-columns); 372 | grid-template-areas: var(--ruff-grid-template-areas); 373 | column-gap: 1rem; 374 | min-height: 100vh; 375 | max-width: 60rem; 376 | margin: 0 auto; 377 | background-color: var(--ruff-layout-background-color); 378 | padding: 0 10px; 379 | } 380 | 381 | .ruff-layout-header { grid-area: toparea; } 382 | .ruff-layout-main {grid-area: mainarea; } 383 | .ruff-layout-nav {grid-area: navarea; } 384 | .ruff-layout-footer {grid-area: botarea; } 385 | 386 | /*** Context free styles ***/ 387 | 388 | h1,h2,h3,h4,h5,h6 { 389 | margin-bottom: 0.5em; 390 | margin-top: 0; 391 | } 392 | 393 | li { 394 | margin-top: 0.5em; 395 | } 396 | 397 | span.ns_scope { 398 | color: var(--ruff-minor-color); 399 | font-size: 85%; 400 | font-weight: bold; 401 | } 402 | 403 | span.ns_scope a[href]:link, span.ns_scope a[href]:visited { 404 | text-decoration: none; 405 | color: var(--ruff-minor-color); 406 | 407 | } 408 | 409 | span.ns_scope a[href]:hover { 410 | text-decoration: underline; 411 | } 412 | 413 | /* Tooltip text - see https://www.w3schools.com/css/css_tooltip.asp */ 414 | .ruff-tip { 415 | position: relative; 416 | /* display: inline-block; */ 417 | } 418 | /* Show the tooltip text when you mouse over the tooltip container */ 419 | .ruff-tip:hover .ruff-tiptext { 420 | visibility: visible; 421 | } 422 | .ruff-tiptext pre { 423 | margin-top: 0; 424 | } 425 | .ruff-tiptext { 426 | min-width: 20em; 427 | text-align: left; 428 | border:none; 429 | position: absolute; 430 | z-index: var(--ruff-tip-z-index); 431 | margin-left: 4px; 432 | padding: 2px 3px; 433 | visibility: hidden; 434 | } 435 | 436 | 437 | /*** Header styles ***/ 438 | 439 | .ruff-hd { 440 | font-family: "Times New Roman", serif; 441 | font-size: 200%; 442 | padding: 5px 0px 10px; 443 | color: var(--ruff-hd-color); 444 | background-color: var(--ruff-hd-background-color); 445 | } 446 | 447 | /*** Footer styles ***/ 448 | 449 | .ruff-ft { 450 | text-align: left; 451 | border-top: 1px solid var(--ruff-ft-color); 452 | color: var(--ruff-ft-color); 453 | background-color: var(--ruff-ft-background-color); 454 | margin: 10px 0px; 455 | } 456 | 457 | .ruff-ft div { 458 | padding: 5px 0px; 459 | } 460 | 461 | /*** Nav pane styles ***/ 462 | 463 | .ruff-nav { 464 | background-color: var(--ruff-nav-background-color); 465 | color: var(--ruff-nav-color); 466 | --ruff-nav-padding-x: 4px; 467 | padding: 3px var(--ruff-nav-padding-x) 2px var(--ruff-nav-padding-x); 468 | font-family: Arial, sans-serif; 469 | /* NOTE: without overflow: visible, tooltips will be clipped */ 470 | overflow: visible; 471 | font-size: 85%; 472 | margin: 0; 473 | } 474 | 475 | .ruff-nav ul { 476 | list-style: none; 477 | margin: 0; 478 | padding: 0; 479 | } 480 | 481 | .ruff-nav li { 482 | margin: 0; 483 | } 484 | 485 | .ruff-nav .ruff-toc1 { 486 | font-weight: bold; /* Override */ 487 | } 488 | .ruff-nav .ruff-toc2 { 489 | padding-left: calc(2em + var(--ruff-nav-toc-offset,0)); 490 | text-indent: -2em; 491 | } 492 | .ruff-nav .ruff-toc3 { 493 | padding-left: calc(3em + var(--ruff-nav-toc-offset,0)); 494 | text-indent: -2em; 495 | } 496 | .ruff-nav .ruff-toc4 { 497 | padding-left: calc(4em + var(--ruff-nav-toc-offset,0)); 498 | text-indent: -2em; 499 | } 500 | .ruff-nav .ruff-toc5 { 501 | padding-left: calc(5em + var(--ruff-nav-toc-offset,0)); 502 | text-indent: -2em; 503 | } 504 | .ruff-nav hr { 505 | color: inherit; 506 | margin-top:0.2em; 507 | margin-bottom:0.2em; 508 | } 509 | 510 | .ruff-nav a:link, .ruff-nav a:visited, .ruff-nav a:hover { 511 | text-decoration: none; 512 | /* Bug #70 Edge/Chrome - color:inherit makes visited link keep browser visited color */ 513 | color: var(--ruff-nav-color); 514 | background-color: inherit; 515 | } 516 | 517 | .ruff-nav a:hover { 518 | /* Disabled because it causes navigation pane width changes. */ 519 | /* Instead reverse foreground/background */ 520 | /* font-weight: bold; */ 521 | color: var(--ruff-nav-background-color); 522 | background-color: var(--ruff-nav-color); 523 | } 524 | 525 | .ruff-nav a.ruff-highlight { 526 | color: var(--ruff-nav-highlight-color); 527 | background-color: var(--ruff-nav-highlight-background-color); 528 | margin-left: calc(-1 * var(--ruff-nav-padding-x)); 529 | padding-left: var(--ruff-nav-padding-x); 530 | padding-right: var(--ruff-nav-padding-x); 531 | } 532 | 533 | .ruff-nav .ruff-tiptext { 534 | background-color: var(--ruff-nav-tip-background-color); 535 | color: var(--ruff-nav-tip-color); 536 | /* Fix Bug #72 */ 537 | text-indent: 0em; 538 | } 539 | 540 | /*** Main content styles ***/ 541 | 542 | .ruff-bd { 543 | font-family: Arial, sans-serif; 544 | color: var(--ruff-bd-color); 545 | background-color: var(--ruff-bd-background-color); 546 | font-size: 93%; 547 | line-height: 1.3; 548 | } 549 | 550 | .ruff-bd .ruff-uplink { 551 | font-size: x-small; 552 | font-variant: normal; 553 | font-family: Arial, sans-serif; 554 | float: right; 555 | padding:2px; 556 | } 557 | 558 | .ruff-bd .ruff-uplink a[href], .ruff-bd .ruff-uplink a[href]:visited { 559 | text-decoration: underline; 560 | /* Bug #70, On Edge/Chrome, needed else shows browser's visited color */ 561 | /* Note color:inherit does not fix this */ 562 | color: var(--ruff-bd-h-color); 563 | } 564 | 565 | .ruff-bd h1 .ruff-uplink a[href], .ruff-bd h1 .ruff-uplink a[href]:visited { 566 | color: var(--ruff-bd-h1-color); 567 | } 568 | 569 | .ruff-bd table.ruff_deflist { 570 | margin: 1em; 571 | margin-top: 0.5em; 572 | border: thin solid; 573 | border-collapse: collapse; 574 | border-color: var(--ruff-bd-table-border); 575 | padding: 4px; 576 | } 577 | 578 | .ruff-bd .ruff_deflist td { 579 | border: thin solid; 580 | border-color: #808080; 581 | vertical-align: top; 582 | font-size: 93%; 583 | padding: 0.3em; 584 | padding-top: 0.1em; 585 | } 586 | 587 | .ruff-bd .ruff_deflist th { 588 | border: thin solid; 589 | border-color: #808080; 590 | padding: 0.3em; 591 | padding-top: 0.1em; 592 | background-color: #CCCCCC; 593 | } 594 | 595 | .ruff-bd h1 { 596 | background-color: var(--ruff-bd-h1-background-color); 597 | color: var(--ruff-bd-h1-color); 598 | font-family: "Times New Roman", serif; 599 | /* Get H1 text to align with other text while having padding from border */ 600 | padding-left: 2px; 601 | margin-left: -2px; 602 | } 603 | 604 | .ruff-bd h2 { 605 | font-variant: small-caps; 606 | color: var(--ruff-bd-h-color); 607 | background-color: var(--ruff-bd-h-background-color); 608 | font-family: "Times New Roman", serif; 609 | } 610 | 611 | .ruff-bd h3, .ruff-bd h4, .ruff-bd h5, .ruff-bd h6 { 612 | color: var(--ruff-bd-h-color); 613 | background-color: var(--ruff-bd-h-background-color); 614 | margin-bottom: 0.2em; 615 | } 616 | 617 | .ruff-bd h5 { 618 | font-style: italic; 619 | font-weight: normal; 620 | font-size: inherit; 621 | } 622 | .ruff-bd h6 { 623 | font-weight: normal; 624 | font-size: inherit; 625 | } 626 | 627 | .ruff-bd h3.ruffclass, .ruff-bd h3.ruffproc, .ruff-bd h3.ruffmethod, 628 | .ruff-bd h4.ruffclass, .ruff-bd h4.ruffproc, .ruff-bd h4.ruffmethod, 629 | .ruff-bd h5.ruffclass, .ruff-bd h5.ruffproc, .ruff-bd h5.ruffmethod { 630 | border-bottom: thin solid; 631 | margin-bottom: 0.2em; 632 | margin-top: 2em; 633 | } 634 | 635 | .ruff-bd code, .ruff-bd .ruff_cmd { 636 | background-color: var(--ruff-bd-code-background-color); 637 | border-radius: 4px; 638 | padding-left: 2px; 639 | padding-right: 2px; 640 | } 641 | 642 | .ruff-bd pre { 643 | color: var(--ruff-bd-code-color); 644 | background-color: var(--ruff-bd-code-background-color); 645 | /* - now inside
which already has a margin 646 | margin-top: 1em; 647 | margin-left: 1em; 648 | */ 649 | padding: 5px; 650 | font-family: Consolas, "Courier New", monospace; 651 | font-size: smaller; 652 | line-height: 1.2em; 653 | white-space: pre-wrap; 654 | overflow-wrap: break-word; 655 | display: inline-block; 656 | text-align: left; 657 | } 658 | 659 | .ruff-bd a[href], .ruff-bd a[href]:visited { 660 | text-decoration: none; 661 | color: var(--ruff-bd-a-color); 662 | } 663 | 664 | .ruff-bd a[href]:hover { 665 | text-decoration: underline; 666 | } 667 | 668 | 669 | .ruff_dyn_src { 670 | display: none; /* Displayed via JS */ 671 | } 672 | 673 | .ruff-bd .ruff_synopsis { 674 | border: var(--ruff-bd-synopsis-border); 675 | color: var(--ruff-bd-code-color); 676 | background-color: var(--ruff-bd-code-background-color); 677 | font-family: Consolas, "Courier New", monospace; 678 | margin: 0em 2em 1em; 679 | padding: 0.5em; 680 | /* Cannot arrange for wrapped line indent since synopsis may have multiple lines 681 | padding-left: 1em; 682 | text-indent: 0.5em; 683 | */ 684 | } 685 | 686 | .ruff-bd .ruff_const, .ruff-bd .ruff_cmd, .ruff-bd code { 687 | font-family: Consolas, "Courier New", monospace; 688 | } 689 | 690 | .ruff-bd .ruff_arg { 691 | font-style: italic; 692 | font-family: Consolas, "Courier New", monospace; 693 | font-size: smaller; 694 | } 695 | 696 | .ruff-bd .ruff_source_link { 697 | font-size: small; 698 | } 699 | 700 | .ruff-bd .ruff_source_link a[href] { 701 | color: var(--ruff-bd-sourcelink-color); 702 | background-color: var(--ruff-bd-sourcelink-background-color); 703 | text-decoration: underline; 704 | } 705 | 706 | /* Specialization for index page body */ 707 | 708 | .ruff_index { 709 | font-size: smaller; 710 | } 711 | 712 | .ruff_index ul li { 713 | list-style-type: none; 714 | } 715 | 716 | .ruff_index ul li a { 717 | text-decoration: none; 718 | } 719 | 720 | #indexUL { 721 | line-height: 1; 722 | } 723 | 724 | .ruff-bd #indexUL .ruff-tiptext, 725 | .ruff-bd #indexUL .ruff-tiptext pre, 726 | .ruff-bd #indexUL .ruff-tiptext pre .ruff_cmd, 727 | .ruff-bd #indexUL .ruff-tiptext pre .ruff_arg { 728 | background-color: var(--ruff-bd-tip-background-color); 729 | color: var(--ruff-bd-tip-color); 730 | } 731 | 732 | /* Classes for figures */ 733 | 734 | .ruff-figure { 735 | margin: 0.5em 1em; 736 | } 737 | .ruff-snippet { 738 | background-color: var(--ruff-bd-code-background-color); 739 | } 740 | 741 | .ruff-left { 742 | text-align: left; 743 | } 744 | .ruff-center { 745 | text-align: center; 746 | } 747 | .ruff-right { 748 | text-align: right; 749 | } 750 | 751 | .ruff-caption { 752 | font-style: italic; 753 | font-size: smaller; 754 | /* Do not want caption in color of ruff-snippet class in
*/ 755 | background-color: var(--ruff-bd-background-color); 756 | } 757 | 758 | .ruff-figure img { 759 | max-width: 100%; 760 | height: auto; 761 | } 762 | 763 | /* Classes for "More" expansion */ 764 | .ruff-expand > span { 765 | font-size: small; 766 | text-decoration: underline; 767 | } 768 | 769 | summary.ruff-expand { 770 | /* list-style-type: none; */ 771 | margin-bottom: 1em; 772 | } 773 | -------------------------------------------------------------------------------- /src/assets/ruff.js: -------------------------------------------------------------------------------- 1 | /* Ruff! JS helpers 2 | To minimize: 3 | 4 | uglifyjs ruff.js -o ruff-min.js 5 | 6 | */ 7 | 8 | function toggleSource( id ) 9 | { 10 | /* Copied from Rails */ 11 | var elem 12 | var link 13 | 14 | if( document.getElementById ) 15 | { 16 | elem = document.getElementById( id ) 17 | link = document.getElementById( "l_" + id ) 18 | } 19 | else if ( document.all ) 20 | { 21 | elem = eval( "document.all." + id ) 22 | link = eval( "document.all.l_" + id ) 23 | } 24 | else 25 | return false; 26 | 27 | if( elem.style.display == "block" ) 28 | { 29 | elem.style.display = "none" 30 | link.innerHTML = "Show source" 31 | } 32 | else 33 | { 34 | elem.style.display = "block" 35 | link.innerHTML = "Hide source" 36 | } 37 | } 38 | 39 | 40 | /*** 41 | * NOTE: for file URL's local storage is per-file so is not maintained between files. 42 | * It will still persist for http urls. 43 | * Someone who actually knows Javascript please rewrite this! 44 | */ 45 | 46 | function ruffSetTheme(themeName) { 47 | localStorage.ruff_theme = themeName; 48 | document.documentElement.className = "ruff-theme-".concat(themeName); 49 | } 50 | 51 | function ruffNextTheme() { 52 | themeNames = JSON.parse(localStorage.ruff_themes); 53 | currentTheme = localStorage.ruff_theme; 54 | if (currentTheme === undefined) { 55 | themeIndex = 0; 56 | } else { 57 | themeIndex = themeNames.indexOf(currentTheme); 58 | ++themeIndex; 59 | if (themeIndex >= themeNames.length) { 60 | themeIndex = 0; 61 | } 62 | } 63 | ruffSetTheme(themeNames[themeIndex]); 64 | } 65 | 66 | function ruffSetNavSide(navSide) { 67 | localStorage.ruff_nav_side = navSide; 68 | but = document.getElementById("ruffNavMove"); 69 | // Note we set individual border properties as we do not want color to change */ 70 | if (navSide === "right") { 71 | gridAreas = '"toparea toparea" "mainarea navarea" "botarea botarea"'; 72 | gridCols = "1fr minmax(200px, min-content)"; 73 | but.textContent = "\u25c0"; 74 | but.style.setProperty("border-left", "none"); 75 | but.style.setProperty("border-right-style", "solid"); 76 | but.style.setProperty("border-right-width", "thick"); 77 | } else { 78 | gridAreas = '"toparea toparea" "navarea mainarea" "botarea botarea"'; 79 | gridCols = "minmax(200px, min-content) 1fr"; 80 | but.textContent = "\u25b6"; 81 | but.style.setProperty("border-right", "none"); 82 | but.style.setProperty("border-left-style", "solid"); 83 | but.style.setProperty("border-left-width", "thick"); 84 | } 85 | document.documentElement.style.setProperty("--ruff-grid-template-areas", gridAreas); 86 | document.documentElement.style.setProperty("--ruff-grid-template-columns", gridCols); 87 | } 88 | 89 | function ruffMoveNavPane() { 90 | if (localStorage.ruff_nav_side === "left") 91 | ruffSetNavSide("right"); 92 | else 93 | ruffSetNavSide("left"); 94 | } 95 | 96 | // Immediately invoked function to set the theme on initial load 97 | (function () { 98 | // Set up the themes 99 | themeNames = ["v1", "light", "dark", "slate", "solar", "clouds", "maroon"]; 100 | // Store list of ruff themes since they may change between releases 101 | // localStorage can only contain strings 102 | localStorage.ruff_themes = JSON.stringify(themeNames); 103 | navSide = localStorage.ruff_nav_side; 104 | if (navSide !== "left" && navSide !== "right") 105 | navSide = "left"; 106 | 107 | // Actual updating of DOM only to be done AFTEr load is done 108 | window.onload = init; 109 | function init () { 110 | currentTheme = localStorage.ruff_theme; 111 | if (currentTheme === undefined || themeNames.indexOf(currentTheme) < 0) { 112 | currentTheme = "v1"; 113 | } 114 | ruffSetTheme(currentTheme); 115 | 116 | // Set up the navigation layout 117 | navSide = localStorage.ruff_nav_side; 118 | if (navSide !== "right") 119 | navSide = "left"; 120 | ruffSetNavSide(navSide); 121 | } 122 | })(); 123 | -------------------------------------------------------------------------------- /src/diagram.tcl: -------------------------------------------------------------------------------- 1 | # Plug-ins for processing diagrams 2 | 3 | namespace eval ruff::diagram { 4 | namespace path [list [namespace parent] [namespace parent]::private] 5 | 6 | namespace eval generators { 7 | namespace path [namespace eval [namespace parent] {namespace path}] 8 | } 9 | 10 | } 11 | 12 | proc ruff::diagram::OBSOLETEparse_command {command} { 13 | # Parses a diagram command 14 | # command - the diagram command line 15 | # 16 | # The first word of the command line is expected to be 17 | # the word "diagram". Following is a list of option value pairs that begin 18 | # character `-` followed by the diagrammer command. 19 | # 20 | # If the diagrammer command is not present, a default is supplied. 21 | # 22 | # Returns a pair consisting of a (possibly empty) option dictionary and the 23 | # diagrammer command. 24 | 25 | set command [lassign $command first] 26 | if {$first ne "diagram"} { 27 | error "Internal error: command is not a diagram." 28 | } 29 | if {[llength $command] == 0} { 30 | return [list [dict create] [program_option -diagrammer]] 31 | } 32 | 33 | set n [llength $command] 34 | set options [dict create] 35 | for {set i 0} {$i < $n} {incr i} { 36 | set option [lindex $command $i] 37 | if {[string index $option 0] ne "-"} { 38 | # End of options 39 | break 40 | } 41 | if {[incr i] == $n} { 42 | error "Missing value to go with option \"[lindex $command $i]\" in diagram." 43 | } 44 | dict set options $option [lindex $command $i] 45 | } 46 | if {$i == $n} { 47 | set diagrammer [program_option -diagrammer] 48 | } else { 49 | set diagrammer [lrange $command $i end] 50 | } 51 | return [list $options $diagrammer] 52 | } 53 | 54 | proc ruff::diagram::generate {text filename generator args} { 55 | variable diagram_counter 56 | 57 | if {$filename eq ""} { 58 | set filename diagram[incr diagram_counter] 59 | } 60 | set url "assets/$filename.svg" 61 | set fd [open [file join [program_option -outdir] $url] wb] 62 | try { 63 | set commands [info commands generators::$generator] 64 | if {[llength $commands] == 1} { 65 | [lindex $commands 0] $fd $text {*}$args 66 | return $url 67 | } 68 | } finally { 69 | close $fd 70 | } 71 | error "Unknown diagram generator \"$generator\"." 72 | } 73 | 74 | ### 75 | # kroki diagrammer 76 | proc ruff::diagram::generators::kroki_init {} { 77 | # If a command line kroki exists, we will use it 78 | if {[llength [auto_execok kroki]]} { 79 | interp alias {} [namespace current]::kroki_generate {} [namespace current]::kroki_generate_cli 80 | proc kroki_init {} {} 81 | return 82 | } 83 | 84 | # If no command line kroki, need to use HTTP over TLS to the online server 85 | uplevel #0 package require http 86 | 87 | # For Windows try twapi first 88 | 89 | if {$::tcl_platform(platform) eq "windows" && 90 | ![catch { uplevel #0 package require twapi_crypto }]} { 91 | http::register https 443 twapi::tls_socket 92 | } else { 93 | uplevel #0 package require tls 94 | tls::init -autoservername true 95 | http::register https 443 tls::socket 96 | } 97 | 98 | # Not windows or no twapi 99 | interp alias {} [namespace current]::kroki_generate {} [namespace current]::kroki_generate_http 100 | proc kroki_init {} {} 101 | return 102 | } 103 | 104 | proc ruff::diagram::generators::kroki_generate_cli {text input_format fd} { 105 | set kroki_fd [open |[list {*}[auto_execok kroki] convert - -f svg -t $input_format -o -] r+] 106 | puts $kroki_fd $text 107 | close $kroki_fd w 108 | puts $fd [read $kroki_fd] 109 | close $kroki_fd 110 | } 111 | 112 | proc ruff::diagram::generators::kroki_generate_http {text input_format fd} { 113 | # See https://wiki.tcl-lang.org/page/dia2kroki 114 | set b64 [string map {+ - / _ = ""} [binary encode base64 [zlib compress $text]]] 115 | set uri https://kroki.io/$input_format/svg/$b64 116 | set tok [http::geturl $uri] 117 | if {[http::status $tok] ne "ok"} { 118 | error "Failed to get image from $uri" 119 | } 120 | puts $fd [http::data $tok] 121 | return 122 | } 123 | 124 | proc ruff::diagram::generators::kroki {fd text {input_format ditaa} args} { 125 | variable kroki_image_counter 126 | kroki_init 127 | kroki_generate $text $input_format $fd 128 | } 129 | 130 | ### 131 | # ditaa diagrammer 132 | 133 | proc ruff::diagram::generators::ditaa {fd text args} { 134 | variable ditaa_image_counter 135 | 136 | set image_fd [open |[list {*}[auto_execok ditaa] - - --svg {*}$args] r+] 137 | puts $image_fd $text 138 | close $image_fd w 139 | puts $fd [read $image_fd] 140 | close $image_fd 141 | } 142 | -------------------------------------------------------------------------------- /src/doctools_formatter.tcl: -------------------------------------------------------------------------------- 1 | error "This file is obsolete!" 2 | # Copyright (c) 2009-2019, Ashok P. Nadkarni 3 | # All rights reserved. 4 | # See the file WOOF_LICENSE in the Woof! root directory for license 5 | 6 | 7 | # Ruff! formatter for doctools 8 | # 9 | # doctools command line: 10 | # ::ruff::document_namespace doctools ::ruff -output ruff.man 11 | # exec tclsh86 c:/bin/tcl86/bin/dtplite.tcl -o ruff.html html ruff.man 12 | # 13 | 14 | 15 | namespace eval ruff::formatter::doctools { 16 | namespace path [list \ 17 | [namespace parent [namespace parent]] \ 18 | [namespace parent [namespace parent]]::private \ 19 | ] 20 | } 21 | 22 | proc ruff::formatter::doctools::escape {s} { 23 | # s - string to be escaped 24 | # Protects a string against doctools substitution in text 25 | # (not to be used inside a doctools command argument as that 26 | # follows Tcl escaping rules and are easiest escaped by enclosing 27 | # in braces) 28 | 29 | # It appears as though the only characters needing replacing are 30 | # [ and ]. Other Tcl special chars ($ \ etc.) do not matter 31 | # return [string map [list \\ \\\\ \[ \[lb\] \] \[rb\] \$ \\\$] $s] 32 | return [string map [list \[ \[lb\] \] \[rb\]] $s] 33 | 34 | } 35 | 36 | 37 | proc ruff::formatter::doctools::_fmtparas {paras} { 38 | # Given a list of paragraph elements, returns 39 | # them appropriately formatted for doctools. 40 | # paras - a flat list of pairs with the first element 41 | # in a pair being the type, and the second the content 42 | # 43 | 44 | set sep "" 45 | set doc "" 46 | # Loop through all the paragraphs 47 | foreach {type content} $paras { 48 | append doc $sep 49 | switch -exact $type { 50 | paragraph { 51 | append doc [escape $content]\n 52 | } 53 | deflist { 54 | append doc [list_begin definitions]\n 55 | foreach {name desc} $content { 56 | append doc "[def [const $name]] [escape $desc]\n" 57 | } 58 | append doc [list_end]\n 59 | } 60 | bulletlist { 61 | append doc [list_begin itemized]\n 62 | foreach desc $content { 63 | append doc "\[item\] [escape $desc]\n" 64 | } 65 | append doc [list_end]\n 66 | } 67 | preformatted { 68 | append doc "\n\[example_begin\]\n" 69 | append doc [escape $content] 70 | append doc "\n\[example_end\]\n" 71 | } 72 | default { 73 | error "Unknown paragraph type '$type'." 74 | } 75 | } 76 | set sep [para] 77 | } 78 | return $doc 79 | } 80 | 81 | proc ruff::formatter::doctools::generate_proc_or_method {procinfo args} { 82 | # Formats the documentation for a proc in doctools format 83 | # procinfo - class information in the format returned 84 | # by extract_ooclass 85 | # 86 | # The following options may be specified: 87 | # -includesource BOOLEAN - if true, the source code of the 88 | # procedure is also included. Default value is false. 89 | # -displayprefix METHODNAME - the string to use as a prefix 90 | # for the method or proc name. Usually caller supplies this 91 | # as the class name for the method. 92 | # -hidenamespace NAMESPACE - if specified as non-empty, 93 | # program element names beginning with NAMESPACE are shown 94 | # with that namespace component removed. 95 | # 96 | # Returns the proc documentation as a doctools formatted string. 97 | 98 | 99 | array set opts {-includesource false -displayprefix "" -hidenamespace ""} 100 | array set opts $args 101 | 102 | array set aproc $procinfo 103 | 104 | set doc "" 105 | 106 | # The quoting of strings below follows what I understand of doctools 107 | # - only [ and ] are special in text outside of doctools commands. 108 | # Such strings are quoted using the escape command. Arguments to 109 | # doctools commands are quoted using {}. 110 | 111 | set itemlist {}; # For the parameter descriptions 112 | set arglist {}; # For the synopsis 113 | # Construct command synopsis 114 | foreach param $aproc(parameters) { 115 | if {[dict get $param type] ne "parameter"} { 116 | # We do not deal with options here 117 | continue 118 | } 119 | set name [dict get $param name] 120 | set item [arg_def {} $name] 121 | if {[dict exists $param description]} { 122 | append item " [escape [dict get $param description]]" 123 | } 124 | if {[dict exists $param default]} { 125 | lappend arglist [opt [arg $name]] 126 | append item " (default \[const {[dict get $param default]}\])" 127 | } else { 128 | lappend arglist [arg $name] 129 | } 130 | lappend itemlist $item 131 | } 132 | set proc_name $opts(-displayprefix)[trim_namespace $aproc(name) $opts(-hidenamespace)] 133 | 134 | if {$aproc(proctype) ne "method"} { 135 | append doc [eval [list call [cmd $proc_name]] $arglist]\n 136 | } else { 137 | switch -exact -- $aproc(name) { 138 | constructor {append doc [eval [list call [cmd "::oo::class create [trim_namespace $aproc(class) $opts(-hidenamespace)]"]] $arglist]} 139 | destructor {append doc [call "[arg OBJECT] [cmd destroy]"]} 140 | default {append doc [eval [list call "[arg OBJECT] [cmd $aproc(name)]"] $arglist]} 141 | } 142 | } 143 | 144 | set sep "" 145 | # Parameter description 146 | if {[llength $itemlist]} { 147 | append doc [list_begin arguments]\n 148 | append doc [join $itemlist \n]\n 149 | append doc [list_end]\n 150 | set sep [para] 151 | } 152 | 153 | # Option description 154 | set itemlist {} 155 | foreach param $aproc(parameters) { 156 | if {[dict get $param type] ne "option"} { 157 | continue 158 | } 159 | set name [dict get $param name] 160 | if {[llength $name] > 1} { 161 | set arg [arg [lrange $name 1 end]] 162 | set name [option [lindex $name 0]] 163 | } else { 164 | set name [option $name] 165 | set arg {} 166 | } 167 | if {[dict exists $param description]} { 168 | set desc [dict get $param description] 169 | } else { 170 | set desc "No description available." 171 | } 172 | lappend itemlist [opt_def $name $arg] [escape $desc] 173 | } 174 | if {[llength $itemlist]} { 175 | append doc $sep 176 | append doc [list_begin options] 177 | append doc [join $itemlist \n]\n 178 | append doc [list_end]\n 179 | set sep [para] 180 | } 181 | 182 | # Loop through all the paragraphs 183 | set paras [_fmtparas $aproc(description)] 184 | if {$paras ne ""} { 185 | append doc $sep$paras 186 | set sep [para] 187 | } 188 | 189 | if {[info exists aproc(return)] && $aproc(return) ne ""} { 190 | append doc $sep 191 | append doc [escape $aproc(return)] 192 | } 193 | 194 | if {$opts(-includesource)} { 195 | append doc $sep 196 | append doc "Source:" 197 | append doc [para] 198 | 199 | # Just [escape...] won't do it. We need the example_begin as well 200 | append doc "\[example_begin\]\n" 201 | append doc [escape $aproc(source)] 202 | append doc "\[example_end\]\n" 203 | } 204 | 205 | 206 | return "${doc}\n" 207 | } 208 | 209 | proc ruff::formatter::doctools::generate_ooclass {classinfo args} { 210 | 211 | # Formats the documentation for a class in doctools format 212 | # classinfo - class information in the format returned 213 | # by extract_ooclass 214 | # 215 | # The following options may be specified: 216 | # -includesource BOOLEAN - if true, the source code of the 217 | # procedure is also included. Default value is false. 218 | # -hidenamespace NAMESPACE - if specified as non-empty, 219 | # program element names beginning with NAMESPACE are shown 220 | # with that namespace component removed. 221 | # 222 | # Returns the class documentation as a doctools formatted string. 223 | 224 | array set opts {-includesource false -hidenamespace ""} 225 | array set opts $args 226 | 227 | array set aclass $classinfo 228 | set doc "" 229 | 230 | # The quoting of strings below follows what I understand of doctools 231 | # - only [ and ] are special in text outside of doctools commands. 232 | # Such strings are quoted using the escape command. Arguments to 233 | # doctools commands are quoted using {}. 234 | 235 | set class_name [trim_namespace $aclass(name) $opts(-hidenamespace)] 236 | set displayprefix "$class_name." 237 | 238 | append doc [section "Class $class_name"] 239 | 240 | if {[info exists aclass(constructor)]} { 241 | append doc [list_begin definitions] 242 | append doc [generate_proc_or_method $aclass(constructor) \ 243 | -includesource $opts(-includesource) \ 244 | -hidenamespace $opts(-hidenamespace) \ 245 | -displayprefix $displayprefix] 246 | append doc [list_end] 247 | } 248 | 249 | 250 | if {[llength $aclass(superclasses)]} { 251 | append doc [subsection "Superclasses"] 252 | # Don't sort - order matters! 253 | append doc [escape [join [trim_namespace_multi $aclass(superclasses) $opts(-hidenamespace)]]]\n 254 | } 255 | if {[llength $aclass(subclasses)]} { 256 | append doc [subsection "Subclasses"] 257 | # Don't sort - order matters! 258 | append doc [escape [join [trim_namespace_multi $aclass(subclasses) $opts(-hidenamespace)]]]\n 259 | } 260 | if {[llength $aclass(mixins)]} { 261 | append doc [subsection "Mixins"] 262 | # Don't sort - order matters! 263 | append doc [escape [join [trim_namespace_multi $aclass(mixins) $opts(-hidenamespace)]]]\n 264 | } 265 | if {[llength $aclass(filters)]} { 266 | append doc [subsection "Filters"] 267 | # Don't sort - order matters! 268 | append doc [escape [join $aclass(filters) ", "]]\n 269 | } 270 | if {[llength $aclass(external_methods)]} { 271 | append doc [subsection "External Methods"] 272 | set external_methods {} 273 | foreach external_method $aclass(external_methods) { 274 | # Qualify the name with the name of the implenting class 275 | foreach {name imp_class} $external_method break 276 | if {$imp_class ne ""} { 277 | set name [trim_namespace $imp_class $opts(-hidenamespace)].$name 278 | } 279 | lappend external_methods $name 280 | } 281 | append doc [escape [join [lsort $external_methods] ", "]]\n 282 | } 283 | 284 | append doc [subsection Methods] 285 | 286 | append doc [list_begin definitions] 287 | if {0} { 288 | # We are showing constructor as part of class definition 289 | if {[info exists aclass(constructor)]} { 290 | append doc [generate_proc_or_method $aclass(constructor) \ 291 | -includesource $opts(-includesource) \ 292 | -hidenamespace $opts(-hidenamespace) \ 293 | -displayprefix $displayprefix] 294 | } 295 | } 296 | if {[info exists aclass(destructor)]} { 297 | append doc [generate_proc_or_method $aclass(destructor) \ 298 | -includesource $opts(-includesource) \ 299 | -hidenamespace $opts(-hidenamespace) \ 300 | -displayprefix $displayprefix] 301 | } 302 | 303 | # We want methods and forwarded methods listed together and sorted 304 | array set methods {} 305 | foreach methodinfo $aclass(methods) { 306 | set methods([dict get $methodinfo name]) [list method $methodinfo] 307 | } 308 | if {[info exists aclass(forwards)]} { 309 | foreach forwardinfo $aclass(forwards) { 310 | set methods([dict get $forwardinfo name]) [list forward $forwardinfo] 311 | } 312 | } 313 | 314 | foreach name [lsort [array names methods]] { 315 | foreach {type info} $methods($name) break 316 | if {$type eq "method"} { 317 | append doc [generate_proc_or_method $info \ 318 | -includesource $opts(-includesource) \ 319 | -hidenamespace $opts(-hidenamespace) \ 320 | -displayprefix $displayprefix] 321 | } else { 322 | # TBD - check formatting of forwarded methods 323 | # append doc [call [cmd "${displayprefix}[trim_namespace [dict get $info name] $opts(-hidenamespace)]"]] 324 | append doc [call "[arg OBJECT] [cmd $name]"] 325 | # TBD - link to forwarded method if possible 326 | append doc "Method forwarded to [cmd [escape [dict get $info forward]]].\n" 327 | } 328 | } 329 | append doc [list_end] 330 | 331 | return $doc 332 | } 333 | 334 | proc ::ruff::formatter::doctools::generate_ooclasses {classinfodict args} { 335 | # Given a list of class information elements returns as string 336 | # containing class documentation formatted for doctools 337 | # classinfodict - dictionary keyed by class name and each element 338 | # of which is in the format returned by extract_ooclass 339 | # 340 | # Additional parameters are passed on to the generate_ooclass procedure. 341 | 342 | set doc "" 343 | foreach name [lsort [dict keys $classinfodict]] { 344 | append doc \ 345 | [eval [list generate_ooclass [dict get $classinfodict $name]] $args] 346 | append doc "\n\n" 347 | } 348 | return $doc 349 | } 350 | 351 | proc ::ruff::formatter::doctools::generate_procs {procinfodict args} { 352 | # Given a dictionary of proc information elements returns a string 353 | # containing documentation formatted for doctools 354 | # procinfodict - dictionary keyed by name of the proc with the associated 355 | # value being in the format returned by extract_proc 356 | # 357 | # Additional parameters are passed on to the generate_proc procedure. 358 | 359 | #ruff 360 | # The returned procedure descriptions are sorted in alphabetical order. 361 | set doc "\[list_begin definitions\]\n" 362 | foreach name [lsort -dictionary [dict keys $procinfodict]] { 363 | append doc \ 364 | [eval [list generate_proc_or_method [dict get $procinfodict $name]] $args]\n\n 365 | } 366 | append doc "\[list_end\]\n" 367 | 368 | return $doc 369 | } 370 | 371 | 372 | proc ::ruff::formatter::doctools::generate_document {classprocinfodict args} { 373 | # Produces documentation in doctools format from the passed in 374 | # class and proc metainformation. 375 | # classprocinfodict - dictionary containing meta information about the 376 | # classes and procs 377 | # 378 | # In addition to options described in the ruff::document command, 379 | # the following additional ones may be specified: 380 | # -preamble DICT - a dictionary indexed by a namespace. Each value is 381 | # a flat list of pairs consisting of a heading and 382 | # corresponding content. These are inserted into the document 383 | # before the actual class and command descriptions for a namespace. 384 | # The key "::" corresponds to documentation to be printed at 385 | # the very beginning. 386 | # -includesource BOOLEAN - if true, the source code of the 387 | # procedure is also included. Default value is false. 388 | # -hidenamespace NAMESPACE - if specified as non-empty, 389 | # program element names beginning with NAMESPACE are shown 390 | # with that namespace component removed. 391 | 392 | array set opts \ 393 | [list \ 394 | -includeclasses true \ 395 | -includeprocs true \ 396 | -includeprivate false \ 397 | -includesource false \ 398 | -hidenamespace "" \ 399 | -section "n" \ 400 | -version "0.0" \ 401 | -name "" \ 402 | -title "" \ 403 | -modulename "" \ 404 | -require {} \ 405 | -author "" \ 406 | -keywords {} \ 407 | -year [clock format [clock seconds] -format %Y] \ 408 | -preamble [dict create] \ 409 | ] 410 | 411 | array set opts $args 412 | 413 | # TBD - does anything need to be escape'ed here? 414 | set doc "\[manpage_begin \"$opts(-name)\" \"$opts(-section)\" \"$opts(-version)\"\]\n" 415 | if {$opts(-author) ne ""} { 416 | append doc "\[copyright {$opts(-year) \"$opts(-author)\"}\]\n" 417 | } 418 | if {$opts(-title) ne ""} { 419 | append doc "\[titledesc \"$opts(-title)\"\]\n" 420 | } 421 | if {$opts(-modulename) ne ""} { 422 | append doc "\[moddesc \"$opts(-modulename)\"\]\n" 423 | } 424 | if {[llength $opts(-require)]} { 425 | foreach require $opts(-require) { 426 | append doc "\[require $require\]\n" 427 | } 428 | } 429 | 430 | # Begin the description section 431 | append doc "\[description\]\n" 432 | 433 | if {[dict exists $opts(-preamble) "::"]} { 434 | # Print the toplevel (global stuff) 435 | foreach {sec paras} [dict get $opts(-preamble) "::"] { 436 | append doc [subsection $sec] 437 | append doc [_fmtparas $paras] 438 | } 439 | } 440 | 441 | set info_by_ns [sift_classprocinfo $classprocinfodict] 442 | 443 | foreach ns [lsort -dictionary [dict keys $info_by_ns]] { 444 | append doc [section "Module $ns"] 445 | if {[dict exists $opts(-preamble) $ns]} { 446 | foreach {sec paras} [dict get $opts(-preamble) $ns] { 447 | append doc [section $sec] 448 | append doc [_fmtparas $paras] 449 | } 450 | } 451 | 452 | if {[dict exists $info_by_ns $ns classes]} { 453 | append doc [section Classes]\n 454 | append doc [generate_ooclasses [dict get $info_by_ns $ns classes] \ 455 | -includesource $opts(-includesource) \ 456 | -hidenamespace $opts(-hidenamespace) \ 457 | ] 458 | } 459 | if {[dict exists $info_by_ns $ns procs]} { 460 | append doc [section Commands]\n 461 | append doc [generate_procs [dict get $info_by_ns $ns procs] \ 462 | -includesource $opts(-includesource) \ 463 | -hidenamespace $opts(-hidenamespace) \ 464 | ] 465 | } 466 | } 467 | 468 | if {[llength $opts(-keywords)] == 0} { 469 | # dtplite will barf if no keywords in man page. Logged on sf.net 470 | # as a bug against doctools 471 | ::ruff::app::log_error "Warning: no keywords specified in this module. If no modules have keywords some versions of the doctools indexer may generate an error in some modes." 472 | } 473 | 474 | if {[llength $opts(-keywords)]} { 475 | append doc [eval keywords $opts(-keywords)] 476 | } 477 | 478 | 479 | append doc "\[manpage_end\]\n" 480 | 481 | return $doc 482 | } 483 | 484 | 485 | proc ruff::formatter::doctools::_fmtcmd {cmd args} { 486 | # Returns a string that is a doctools command escaped appropriately 487 | set arglist {} 488 | foreach arg $args { 489 | if {[string index $arg 0] eq "\["} { 490 | # Do not quote if nested command 491 | lappend arglist $arg 492 | } elseif {[regexp {^[[:alnum:]_-]$} $arg]} { 493 | # Simple word, do not quote unnecessarily 494 | lappend arglist $arg 495 | } else { 496 | # Quote in case there are special characters 497 | # TBD - do we need to escape as well ? Despite what the 498 | # doctools syntax page says, the actual syntax rules do 499 | # not seem exactly those of Tcl 500 | lappend arglist "\"$arg\"" 501 | } 502 | } 503 | return "\[$cmd [join $arglist { }]\]" 504 | } 505 | 506 | proc ruff::formatter::doctools::_fmtcmdnl {args} { 507 | return [eval _fmtcmd $args]\n 508 | } 509 | 510 | interp alias {} ::ruff::formatter::doctools::cmd {} ::ruff::formatter::doctools::_fmtcmd cmd 511 | interp alias {} ::ruff::formatter::doctools::section {} ::ruff::formatter::doctools::_fmtcmdnl section 512 | interp alias {} ::ruff::formatter::doctools::subsection {} ::ruff::formatter::doctools::_fmtcmdnl subsection 513 | interp alias {} ::ruff::formatter::doctools::list_begin {} ::ruff::formatter::doctools::_fmtcmdnl list_begin 514 | interp alias {} ::ruff::formatter::doctools::list_end {} ::ruff::formatter::doctools::_fmtcmdnl list_end 515 | interp alias {} ::ruff::formatter::doctools::call {} ::ruff::formatter::doctools::_fmtcmdnl call 516 | interp alias {} ::ruff::formatter::doctools::para {} ::ruff::formatter::doctools::_fmtcmdnl para 517 | interp alias {} ::ruff::formatter::doctools::def {} ::ruff::formatter::doctools::_fmtcmdnl def 518 | interp alias {} ::ruff::formatter::doctools::arg {} ::ruff::formatter::doctools::_fmtcmd arg 519 | interp alias {} ::ruff::formatter::doctools::arg_def {} ::ruff::formatter::doctools::_fmtcmdnl arg_def 520 | interp alias {} ::ruff::formatter::doctools::opt_def {} ::ruff::formatter::doctools::_fmtcmdnl opt_def 521 | interp alias {} ::ruff::formatter::doctools::const {} ::ruff::formatter::doctools::_fmtcmd const 522 | interp alias {} ::ruff::formatter::doctools::opt {} ::ruff::formatter::doctools::_fmtcmd opt 523 | interp alias {} ::ruff::formatter::doctools::option {} ::ruff::formatter::doctools::_fmtcmd option 524 | interp alias {} ::ruff::formatter::doctools::keywords {} ::ruff::formatter::doctools::_fmtcmd keywords 525 | 526 | -------------------------------------------------------------------------------- /src/formatter_html.tcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019-2022, Ashok P. Nadkarni 2 | # All rights reserved. 3 | # See the file LICENSE in the source root directory for license. 4 | 5 | namespace eval ruff::formatter {} 6 | 7 | oo::class create ruff::formatter::Html { 8 | superclass ::ruff::formatter::Formatter 9 | 10 | # Data members 11 | variable Document; # Current document 12 | variable DocumentNamespace; # Namespace being documented 13 | variable Header; # Common header 14 | variable Footer; # Common footer 15 | variable NavigationLinks; # Navigation links forming ToC 16 | variable HeaderLevels; # Header levels for various headers 17 | variable CssClasses; # CSS classes for various elements 18 | variable GlobalIndex; # Like NavigationLinks but across *all* documents 19 | 20 | constructor args { 21 | set HeaderLevels { 22 | class 3 23 | proc 3 24 | method 4 25 | nonav 5 26 | parameters 5 27 | } 28 | set CssClasses { 29 | class ruffclass 30 | proc ruffproc 31 | method ruffmethod 32 | } 33 | set GlobalIndex [dict create] 34 | next {*}$args 35 | } 36 | 37 | method NewSourceId {} { 38 | # Returns a new id to use for a source listing. 39 | variable SourceIdCounter 40 | if {![info exists SourceIdCounter]} { 41 | set SourceIdCounter 0 42 | } 43 | return [incr SourceIdCounter] 44 | } 45 | 46 | method Anchor args { 47 | # Construct an anchor from the passed arguments. 48 | # args - String from which the anchor is to be constructed. 49 | # The anchor is formed by joining the passed strings with separators. 50 | # Empty arguments are ignored. 51 | # Returns an HTML-escaped anchor without the `#` prefix. 52 | set parts [lmap arg $args { 53 | if {$arg eq ""} continue 54 | my Escape $arg 55 | }] 56 | return [join $parts -] 57 | } 58 | 59 | method HeadingReference {ns heading} { 60 | # Implements the [Formatter.HeadingReference] method for HTML. 61 | return "[ns_file_base $ns]#[my Anchor $ns $heading]" 62 | } 63 | 64 | method FigureReference {ns caption} { 65 | # Returns a link name to use for a figure 66 | return "[ns_file_base $ns]#[my Anchor $ns $caption]" 67 | } 68 | 69 | 70 | method SymbolReference {ns symbol} { 71 | # Implements the [Formatter.SymbolReference] method for HTML. 72 | set ref [ns_file_base $ns] 73 | # Reference to the global namespace is to the file itself. 74 | if {$ns eq "::" && $symbol eq ""} { 75 | return $ref 76 | } 77 | return [append ref "#[my Anchor $symbol]"] 78 | } 79 | 80 | method Begin {} { 81 | # Implements the [Formatter.Begin] method for HTML. 82 | 83 | next 84 | 85 | # Generate the header used by all files 86 | # set Header {} 87 | set Header "" 88 | append Header "\n" 89 | # append Header "" 90 | set titledesc [my Option -title] 91 | append Header "$titledesc\n" 92 | 93 | if {[my Option -linkassets 1]} { 94 | append Header [my LinkAsset ruff-min.css ruff.css] 95 | append Header [my LinkAsset ruff-min.js ruff.js] 96 | } else { 97 | append Header [my GetAsset ruff-min.css ruff.css] 98 | append Header [my GetAsset ruff-min.js ruff.js] 99 | } 100 | 101 | append Header "\n" 102 | 103 | # If we are not splitting pages, h1 headings are shown in navigation alongside 104 | # h2.. headers so the latter need to be offset further. 105 | if {[my Option -pagesplit none] eq "none"} { 106 | append Header "\n" 107 | } else { 108 | append Header "\n"; # Take default of 0 defined in ruff css 109 | } 110 | append Header "
\n" 111 | 112 | append Header "
\n" 113 | if {$titledesc ne ""} { 114 | if {[my Option? -version version]} { 115 | append Header "$titledesc (v$version)\n\n" 116 | } else { 117 | append Header "$titledesc\n\n" 118 | } 119 | } 120 | # Theme control button 121 | append Header { 122 |
123 | 124 | 125 |
126 | } 127 | append Header
128 | 129 | # Generate the Footer used by all files 130 | append Footer "
" 131 | append Footer "
Document generated by Ruff!
" 132 | if {[my Option? -copyright copyright]} { 133 | append Footer "
© [my Escape $copyright]
" 134 | } 135 | append Footer "
\n" 136 | 137 | append Footer "
" 138 | 139 | return 140 | } 141 | 142 | method DocumentBegin {ns} { 143 | # See [Formatter.DocumentBegin]. 144 | # ns - Namespace for this document. 145 | 146 | next $ns 147 | 148 | set NavigationLinks [dict create] 149 | set Document $Header 150 | append Document "
" 151 | set DocumentNamespace $ns 152 | 153 | return 154 | } 155 | 156 | method DocumentEnd {} { 157 | # See [Formatter.DocumentEnd]. 158 | 159 | # Close off
from DocumentBegin 160 | #append Document "
" 161 | 162 | append Document "
" 163 | 164 | # Add the navigation bits and footer 165 | my Navigation $DocumentNamespace 166 | append Document $Footer 167 | 168 | next 169 | 170 | set doc $Document 171 | set Document "" 172 | return $doc 173 | } 174 | 175 | method DocumentIndex {} { 176 | # See [Formatter.DocumentIndex] 177 | # references - namespace keyed nested dictionary 178 | # 179 | my DocumentBegin Index 180 | 181 | set entries {} 182 | dict for {key link} $GlobalIndex { 183 | lappend entries [dict get $link label] $link 184 | } 185 | set entries [lsort -stride 2 -dictionary $entries] 186 | 187 | append Document "

Index

" 188 | append Document "

\n" 189 | append Document {} 190 | append Document { 191 |
192 |
    193 |
  • Type the index terms you want to search for in the text input field. 194 |
  • Matching terms will be shown incrementally as you type. 195 |
  • Press Enter to navigate to the target of the first displayed 196 | index entry. 197 |
  • Alternatively, Tab to move to the index entry of interest and then press 198 | Enter to navigate to that documentation page. 199 |
  • To jump to this page from any other documentation page, 200 | press browser-specific shortcut modifiers with i. 201 | For example, on IE and Edge this would be 202 | Alt-i while on Firefox and Chrome Alt-Shift-i. 203 | Other browsers and platforms may differ. 204 |
205 |
206 | } 207 | append Document "\n
    \n" 208 | 209 | foreach {label link} $entries { 210 | set label [my Escape [string trimleft $label :]] 211 | # set tag [dict get $link tag] 212 | set tag li 213 | set href [dict get $link href] 214 | set ns "" 215 | if {[dict exists $link ns]} { 216 | set ns [dict get $link ns] 217 | if {$ns ne ""} { 218 | set ns " [my Escape $ns]" 219 | } 220 | } 221 | if {[dict exists $link tip]} { 222 | append Document "<$tag class='ruff-tip'>$label[dict get $link tip]$ns" 223 | } else { 224 | append Document "<$tag>$label$ns" 225 | } 226 | } 227 | append Document "\n
\n" 228 | append Document "
" 229 | append Document [my GetAsset ruff-index-min.js ruff-index.js] 230 | append Document "\n" 231 | 232 | return [my DocumentEnd] 233 | } 234 | 235 | method AddProcedureDetail {procinfo} { 236 | # Adds the detailed information about a procedure or method 237 | # procinfo - dictionary describing the procedure. See [AddProcedure] 238 | # 239 | # The concrete implementation can override this. 240 | 241 | if {[my Option -compact 0]} { 242 | append Document "
Details\n" 243 | } 244 | next $procinfo 245 | if {[my Option -compact 0]} { 246 | append Document "
\n" 247 | } 248 | } 249 | 250 | method AddProgramElementHeading {type fqn {tooltip {}} {synopsis {}}} { 251 | # Adds heading for a program element like procedure, class or method. 252 | # type - One of `proc`, `class` or `method` 253 | # fqn - Fully qualified name of element. 254 | # tooltip - The tooltip lines, if any, to be displayed in navigation pane. 255 | # synopsis - The synopsis to be displayed along with tooltip. Alternating 256 | # list of command name and argument list. 257 | # In addition to adding the heading to the document, a link 258 | # is also added to the collection of navigation links. 259 | 260 | set level [dict get $HeaderLevels $type] 261 | set ns [namespace qualifiers $fqn] 262 | set anchor [my Anchor $fqn] 263 | set href [my SymbolReference $ns $fqn] 264 | set linkinfo [dict create level $level href $href ns $ns] 265 | 266 | # Construct tooltip from synopsis and tooltip 267 | if {[llength $synopsis]} { 268 | set tip "
[join [my SynopsisToHtml $synopsis] \n]
" 269 | } 270 | if {[llength $tooltip]} { 271 | append tip "[my ToHtml [string trim [join $tooltip { }]] $ns]\n" 272 | } 273 | if {[info exists tip]} { 274 | dict set linkinfo tip $tip 275 | } 276 | 277 | set name [namespace tail $fqn] 278 | dict set linkinfo label $name 279 | dict set NavigationLinks $anchor [dict create LinkInfo $linkinfo Type $type] 280 | dict set GlobalIndex $anchor $linkinfo 281 | if {[string length $ns]} { 282 | set ns_link [my ToHtml [markup_reference $ns]] 283 | set heading "[my Escape $name] \[${ns_link}\]" 284 | } else { 285 | set heading "[my Escape $fqn]" 286 | } 287 | append Document [my HeadingWithUplink $level $heading $ns [dict get $CssClasses $type]] 288 | return 289 | } 290 | 291 | method AddHeading {level text scope {tooltip {}}} { 292 | # See [Formatter.AddHeading]. 293 | # level - The numeric or semantic heading level. 294 | # text - The heading text. 295 | # scope - The documentation scope of the content. 296 | # tooltip - Tooltip to display in navigation link. 297 | 298 | if {![string is integer -strict $level]} { 299 | set level [dict get $HeaderLevels $level] 300 | } 301 | 302 | set do_link [expr {$level >= [dict get $HeaderLevels nonav] ? false : true}] 303 | 304 | if {$do_link} { 305 | set anchor [my Anchor $scope $text] 306 | set linkinfo [dict create level $level href "#$anchor"] 307 | if {$tooltip ne ""} { 308 | set tip "[my ToHtml [string trim [join $tooltip { }]] $scope]\n" 309 | dict set linkinfo tip $tip 310 | } 311 | dict set linkinfo label $text 312 | 313 | # NOTE: empty because the text itself may contain anchors. 314 | set heading "[my ToHtml $text $scope]" 315 | 316 | # Namespace headers do not get a navigation link if page splitting 317 | # because they are already highlighted in the namespaces section in 318 | # navigation. Hack - this assumes level 1 heading is not used within 319 | # the content. 320 | if {$level > 1 || [my Option -pagesplit none] eq "none"} { 321 | 322 | dict set NavigationLinks $anchor [dict create LinkInfo $linkinfo Type heading] 323 | } 324 | } else { 325 | set heading [my ToHtml $text $scope] 326 | } 327 | append Document [my HeadingWithUplink $level $heading $scope] 328 | return 329 | } 330 | 331 | method AddParagraph {lines scope} { 332 | # See [Formatter.AddParagraph]. 333 | # lines - The paragraph lines. 334 | # scope - The documentation scope of the content. 335 | append Document "

[my ToHtml [string trim [join $lines { }]] $scope]

\n" 336 | return 337 | } 338 | 339 | method AddDefinitions {definitions scope {preformatted none}} { 340 | # See [Formatter.AddDefinitions]. 341 | # definitions - List of definitions. 342 | # scope - The documentation scope of the content. 343 | # preformatted - One of `none`, `both`, `term` or `definition` 344 | # indicating which fields of the definition are 345 | # are already formatted. 346 | append Document "\n" 347 | foreach item $definitions { 348 | set def [join [dict get $item definition] " "] 349 | if {[my Option -autopunctuate 0]} { 350 | set def [string toupper $def 0 0] 351 | if {[regexp {[[:alnum:]]} [string index $def end]]} { 352 | append def "." 353 | } 354 | } 355 | if {$preformatted in {none term}} { 356 | set def [my ToHtml $def $scope] 357 | } 358 | set term [dict get $item term] 359 | if {$preformatted in {none definition}} { 360 | set term [my ToHtml $term $scope] 361 | } 362 | append Document "\n" 367 | } 368 | append Document "
" \ 363 | "$term" \ 364 | "" \ 365 | $def \ 366 | "
\n" 369 | return 370 | } 371 | 372 | method AddBullets {bullets scope} { 373 | # See [Formatter.AddBullets]. 374 | # bullets - The list of bullets. 375 | # scope - The documentation scope of the content. 376 | append Document "
    \n" 377 | foreach lines $bullets { 378 | append Document "
  • [my ToHtml [join $lines { }] $scope]
  • \n" 379 | } 380 | append Document "
\n" 381 | return 382 | } 383 | 384 | method AddPreformattedText {text scope} { 385 | # See [Formatter.AddPreformattedText]. 386 | # text - Preformatted text. 387 | # scope - The documentation scope of the content. 388 | append Document "
\n" \
389 |             [my Escape $text] \
390 |             "\n
\n" 391 | return 392 | } 393 | 394 | method AddFenced {lines fence_options scope} { 395 | # Adds a list of fenced lines to document content. 396 | # lines - Preformatted text as a list of lines. 397 | # fence_options - options controlling generation and layout 398 | # scope - The documentation scope of the content. 399 | 400 | # See if it is a modifier we specialize, else just pass 401 | # it to default implementation. 402 | 403 | if {[dict exists $fence_options -caption]} { 404 | set caption [dict get $fence_options -caption] 405 | set id "id='[my Anchor $scope $caption]'" 406 | if {[my ResolvableReference? $caption $scope ref] && [dict exists $ref label]} { 407 | # May have "Figure X" added 408 | set display_caption [dict get $ref label] 409 | } else { 410 | set display_caption $caption 411 | } 412 | } else { 413 | set caption "" 414 | set display_caption "" 415 | set id "" 416 | } 417 | 418 | set fig_classes ruff-figure 419 | if {[dict exists $fence_options -align]} { 420 | append fig_classes " ruff-[dict get $fence_options -align]" 421 | } 422 | if {[dict exists $fence_options Command] && 423 | [lindex [dict get $fence_options Command] 0] eq "diagram"} { 424 | set diagrammer [lrange [dict get $fence_options Command] 1 end] 425 | if {[llength $diagrammer] == 0} { 426 | set diagrammer [program_option -diagrammer] 427 | } 428 | append Document "\n
" 429 | set image_url [ruff::diagram::generate \ 430 | [join $lines \n] \ 431 | [ruff::private::sanitize_filename $caption] \ 432 | {*}$diagrammer] 433 | append Document "\n" 434 | } else { 435 | append Document "\n
" 436 | append Document [my AddPreformattedText [join $lines \n] $scope] 437 | } 438 | if {$display_caption ne ""} { 439 | append Document "\n
$display_caption
" 440 | } 441 | append Document "\n
" 442 | return 443 | } 444 | 445 | method SynopsisToHtml {synopsis} { 446 | # Returns the a list of HTML lines for a synopsis 447 | # synopsis - List of alternating elements comprising the command portion 448 | # and the parameter list for it. 449 | set lines [list ] 450 | foreach {cmds params} $synopsis { 451 | set cmds "[my Escape [join $cmds { }]]" 452 | if {[llength $params]} { 453 | set params "[my Escape [join $params { }]]" 454 | } else { 455 | set params "" 456 | } 457 | lappend lines "$cmds $params" 458 | } 459 | return $lines 460 | } 461 | 462 | method AddSynopsis {synopsis scope} { 463 | # Adds a Synopsis section to the document content. 464 | # synopsis - List of alternating elements comprising the command portion 465 | # and the parameter list for it. 466 | # scope - The documentation scope of the content. 467 | set lines [my SynopsisToHtml $synopsis] 468 | append Document "
[join $lines
]
\n" 469 | return 470 | } 471 | 472 | method AddSource {source scope procname} { 473 | # Adds a Source code section to the document content. 474 | # source - Source code fragment. 475 | # scope - The documentation scope of the content. 476 | # procname - proc name used to generate stable id - see bug #78 477 | 478 | # See bug #78 set src_id [my NewSourceId] 479 | set src_id [string map {:: _} $procname] 480 | set src_id [string map {: _ \" _ < _ > _ # _ $ _ ? _ ! _ . _ ( _ ) _} $src_id] 481 | set src_id [string trimleft $src_id _] 482 | if {$src_id eq {}} { 483 | set src_id [my NewSourceId] 484 | } 485 | append Document "
" 486 | append Document "\n" 489 | append Document "
[my Escape $source]
\n" 490 | append Document "
"; # class='ruff_source' 491 | 492 | return 493 | } 494 | 495 | method Navigation {{highlight_ns {}}} { 496 | # Adds the navigation box to the document. 497 | # highlight_ns - Namespace to be highlighted in navigation. 498 | 499 | set main_title "Start page" 500 | set main_ref [ns_file_base {}] 501 | set index_ref [ns_file_base -docindex] 502 | 503 | set scrolling "" 504 | foreach opt [my Option -navigation {}] { 505 | switch -exact -- $opt { 506 | scrolled { set scrolling "" } 507 | fixed - 508 | sticky { set scrolling "style='position: sticky; top: 0;'" } 509 | } 510 | } 511 | 512 | append Document ""; 574 | return 575 | } 576 | 577 | method HeadingWithUplink {level heading scope {cssclass ruff}} { 578 | # Returns the HTML fragment wrapping the given heading. 579 | # level - heading level 580 | # heading - bare HTML fragment to use for heading 581 | # scope - the namespace scope to be used for uplinks 582 | # 583 | # If the heading level is less than 5, links to the namespace 584 | # and documentation top are inserted into the heading. 585 | 586 | set hlevel "h$level" 587 | if {$level >= 5} { 588 | # Plain header 589 | return "<$hlevel class='$cssclass'>$heading" 590 | } 591 | 592 | # If the scope is the document top scope, no need to add uplink since 593 | # the "Top" link is essentially the same. 594 | if {$scope ne "" && $scope ne $DocumentNamespace && [my Reference? $scope scope_ref]} { 595 | set links "[namespace tail $scope], " 596 | } 597 | if {[my Option -pagesplit none] eq "none"} { 598 | append links "Top" 599 | } else { 600 | append links \ 601 | "Top, " \ 602 | "Main" 603 | if {[my Option -makeindex true]} { 604 | append links ", Index" 605 | } 606 | } 607 | set links "$links" 608 | # NOTE: the div needed to reset the float from links 609 | return "<$hlevel class='$cssclass'>$heading$links\n
\n" 610 | } 611 | 612 | method Escape {s} { 613 | # Returns an HTML-escaped string. 614 | # s - string to be escaped 615 | # Protects characters in $s against interpretation as 616 | # HTML special characters. 617 | # 618 | # Returns the escaped string 619 | 620 | return [string map { 621 | & & 622 | \" " 623 | < < 624 | > > 625 | } $s] 626 | } 627 | 628 | method LinkAsset {asset args} { 629 | # Returns HTML to be included to link to an asset 630 | # asset - the name of asset to be included 631 | # args - files to check for ensuring $asset is up to date 632 | # 633 | set path [file join [ruff_dir] assets $asset] 634 | foreach arg $args { 635 | set arg [file join [ruff_dir] assets $arg] 636 | if {[file exists $arg] && [file mtime $arg] > [file mtime $path]} { 637 | error "Asset $arg is newer than $path. Regenerate $path." 638 | } 639 | } 640 | if {[file extension $asset] eq ".css"} { 641 | return "\n" 642 | } else { 643 | return "\n" 644 | } 645 | } 646 | 647 | method GetAsset {asset args} { 648 | # Returns HTML to be included for an asset 649 | # asset - the name of asset to be included 650 | # args - files to check for ensuring $asset is up to date 651 | # 652 | set path [file join [ruff_dir] assets $asset] 653 | foreach arg $args { 654 | set arg [file join [ruff_dir] assets $arg] 655 | if {[file exists $arg] && [file mtime $arg] > [file mtime $path]} { 656 | error "Asset $arg is newer than $path. Regenerate $path." 657 | } 658 | } 659 | 660 | if {[file extension $asset] eq ".css"} { 661 | return "\n" 662 | } else { 663 | return "\n" 664 | } 665 | } 666 | 667 | method copy_assets {outdir} { 668 | # Copies any CSS and Javascript assets to the output directory. 669 | # outdir - root directory where output files will be stored 670 | # 671 | 672 | file mkdir [file join $outdir assets] 673 | foreach fn {ruff-min.css ruff-min.js ruff-index-min.js} { 674 | file copy -force [file join [ruff_dir] assets $fn] [file join $outdir assets $fn] 675 | } 676 | } 677 | 678 | method extension {} { 679 | # Returns the default file extension to be used for output files. 680 | return html 681 | } 682 | 683 | forward FormatInline my ToHtml 684 | } 685 | -------------------------------------------------------------------------------- /src/formatter_markdown.tcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, Ashok P. Nadkarni 2 | # All rights reserved. 3 | # See the file LICENSE in the source root directory for license. 4 | 5 | # Ruff! formatter for markdown 6 | # For compiling generated markdown to html using pandoc: 7 | # pandoc -s -o ruff.html -c ../ruff-md.css --metadata pagetitle="My package" ruff.md 8 | # For compiling generated markdown to manpages using pandoc 9 | # pandoc ruff_ruff.md -s -t man -o ruff.man 10 | # dos2unix ruff.man 11 | # tbl ruff.man | man -l - 12 | 13 | namespace eval ruff::formatter {} 14 | 15 | oo::class create ruff::formatter::Markdown { 16 | superclass ::ruff::formatter::Formatter 17 | 18 | # Data members 19 | variable Document; # Current document 20 | variable DocumentNamespace; # Namespace being documented 21 | variable Header; # Common header 22 | variable Footer; # Common footer 23 | variable HeaderLevels; # Header levels for various headers 24 | 25 | # NOTE: NavigationLinks are currently recorded but not used since 26 | # there is no standard way to have a navigation pane or ToC in 27 | # Markdown without resorting to HTML. 28 | variable NavigationLinks; # Navigation links forming ToC 29 | 30 | constructor args { 31 | set HeaderLevels { 32 | class 3 33 | proc 4 34 | method 4 35 | nonav 5 36 | parameters 5 37 | } 38 | next {*}$args 39 | } 40 | 41 | method Anchor args { 42 | # Construct an anchor from the passed arguments. 43 | # args - String from which the anchor is to be constructed. 44 | # The anchor is formed by joining the passed strings with separators. 45 | # Empty arguments are ignored. 46 | # Returns an HTML-escaped anchor without the `#` prefix. 47 | set parts [lmap arg $args { 48 | if {$arg eq ""} continue 49 | set arg 50 | }] 51 | 52 | return [regsub -all {[^-:\w_.]} [join $parts -] _] 53 | } 54 | 55 | method HeadingReference {ns heading} { 56 | # Implements the [Formatter.HeadingReference] method for Markdown. 57 | return "[ns_file_base $ns .html]#[my Anchor $ns $heading]" 58 | } 59 | 60 | method SymbolReference {ns symbol} { 61 | # Implements the [Formatter.SymbolReference] method for Markdown. 62 | set ref [ns_file_base $ns .html] 63 | # Reference to the global namespace is to the file itself. 64 | if {$ns eq "::" && $symbol eq ""} { 65 | return $ref 66 | } 67 | return [append ref "#[my Anchor $symbol]"] 68 | } 69 | 70 | method FigureReference {ns caption} { 71 | # Implements the [Formatter.FigureReference] method for Markdown. 72 | return "[ns_file_base $ns .html]#[my Anchor $ns $caption]" 73 | } 74 | 75 | method Begin {} { 76 | # Implements the [Formatter.Begin] method for HTML. 77 | 78 | next 79 | 80 | # Generate the header used by all files 81 | # Currently, it is empty but might change in the future with 82 | # support for specific dialects which implement metainformation. 83 | set Header "" 84 | set titledesc [my Option -title] 85 | 86 | # Generate the Footer used by all files 87 | set Footer "" 88 | if {[my Option? -copyright copyright]} { 89 | append Footer "\n\n---\n\\(c) [my Escape $copyright]\n" 90 | } 91 | return 92 | } 93 | 94 | method DocumentBegin {ns} { 95 | # See [Formatter.DocumentBegin]. 96 | # ns - Namespace for this document. 97 | 98 | next $ns 99 | 100 | set NavigationLinks [dict create] 101 | set Document $Header 102 | set DocumentNamespace $ns 103 | 104 | return 105 | } 106 | 107 | method DocumentEnd {} { 108 | # See [Formatter.DocumentEnd]. 109 | 110 | # Add the navigation bits and footer 111 | my Navigation $DocumentNamespace 112 | append Document $Footer 113 | 114 | set doc $Document 115 | set Document "" 116 | 117 | next 118 | 119 | return $doc 120 | } 121 | 122 | method AddProgramElementHeading {type fqn {tooltip {}} {synopsis {}}} { 123 | # Adds heading for a program element like procedure, class or method. 124 | # type - One of `proc`, `class` or `method` 125 | # fqn - Fully qualified name of element. 126 | # tooltip - The tooltip lines, if any, to be displayed in the navigation pane. 127 | # In addition to adding the heading to the document, a link 128 | # is also added to the collection of navigation links. 129 | 130 | set level [dict get $HeaderLevels $type] 131 | set atx [string repeat # $level] 132 | set ns [namespace qualifiers $fqn] 133 | set anchor [my Anchor $fqn] 134 | set linkinfo [dict create tag h$level href "#$anchor"] 135 | if {[llength $tooltip]} { 136 | set tip "[my ToMarkdown [string trim [join $tooltip { }]] $ns]\n" 137 | dict set linkinfo tip $tip 138 | } 139 | set name [namespace tail $fqn] 140 | dict set linkinfo label $name 141 | dict set NavigationLinks $anchor $linkinfo 142 | append Document "\n$atx " 143 | if {[string length $ns]} { 144 | set ns_link [my ToMarkdown [markup_reference $ns]] 145 | append Document \ 146 | [my Escape [namespace tail $name]] \ 147 | " \[${ns_link}\]\n" 148 | } else { 149 | append Document [my Escape $name] "\n" 150 | } 151 | return 152 | } 153 | 154 | method AddHeading {level text scope {tooltip {}}} { 155 | # See [Formatter.AddHeading]. 156 | # level - The numeric or semantic heading level. 157 | # text - The heading text. 158 | # scope - The documentation scope of the content. 159 | # tooltip - Tooltip to display in navigation link. 160 | 161 | if {![string is integer -strict $level]} { 162 | set level [dict get $HeaderLevels $level] 163 | } 164 | set do_link [expr {$level >= [dict get $HeaderLevels nonav] ? false : true}] 165 | set atx [string repeat # $level] 166 | 167 | if {$do_link} { 168 | set anchor [my Anchor $scope $text] 169 | set linkinfo [dict create tag h$level href "#$anchor"] 170 | if {$tooltip ne ""} { 171 | set tip "[my ToMarkdown [join $tooltip { }] $scope]\n" 172 | dict set linkinfo tip $tip 173 | } 174 | dict set linkinfo label $text 175 | dict set NavigationLinks $anchor $linkinfo 176 | # NOTE: empty because the text itself may contain anchors. 177 | set heading "[my ToMarkdown $text $scope]" 178 | } else { 179 | set heading [my ToMarkdown $text $scope] 180 | } 181 | append Document "\n" $atx " " $heading "\n" 182 | return 183 | } 184 | 185 | method AddParagraph {lines scope} { 186 | # See [Formatter.AddParagraph]. 187 | # lines - The paragraph lines. 188 | # scope - The documentation scope of the content. 189 | append Document "\n" [my ToMarkdown [join $lines \n] $scope] "\n" 190 | return 191 | } 192 | 193 | method AddDefinitions {definitions scope {preformatted none}} { 194 | # See [Formatter.AddDefinitions]. 195 | # definitions - List of definitions. 196 | # scope - The documentation scope of the content. 197 | # preformatted - One of `none`, `both`, `term` or `definition` 198 | # indicating which fields of the definition are 199 | # are already formatted. 200 | 201 | if {0} { 202 | # This does not escape <> properly. Moreover, cmark seems 203 | # to handle `` within html tags differently depending on whether 204 | # the tag is (e.g.) or 205 | append Document "\n\n" 206 | foreach item $definitions { 207 | set def [join [dict get $item definition] " "] 208 | # Note: since we are generating raw HTML here, we have to 209 | # use ToHtml and not ToMarkdown here. Huh? TBD 210 | if {$preformatted in {none term}} { 211 | set def [my ToMarkdown $def $scope] 212 | } 213 | set term [dict get $item term] 214 | if {$preformatted in {none definition}} { 215 | set term [my ToMarkdown $term $scope] 216 | } 217 | append Document "\n" 222 | } 223 | append Document "
" \ 218 | $term \ 219 | "" \ 220 | $def \ 221 | "
\n" 224 | } else { 225 | # Note: CommonMark does not recognize tables without a heading line 226 | # TBD - how do empty headers look in generated HTML? 227 | append Document "\n|||\n|----|----|\n" 228 | foreach item $definitions { 229 | set def [join [dict get $item definition] " "] 230 | if {[my Option -autopunctuate 0]} { 231 | set def [string toupper $def 0 0] 232 | if {[regexp {[[:alnum:]]} [string index $def end]]} { 233 | append def "." 234 | } 235 | } 236 | if {$preformatted in {none term}} { 237 | set def [my ToMarkdown $def $scope] 238 | } 239 | set term [dict get $item term] 240 | if {$preformatted in {none definition}} { 241 | set term [my ToMarkdown $term $scope] 242 | } 243 | append Document "|$term|$def|\n" 244 | } 245 | append Document "\n" 246 | } 247 | return 248 | } 249 | 250 | method AddBullets {bullets scope} { 251 | # See [Formatter.AddBullets]. 252 | # bullets - The list of bullets. 253 | # scope - The documentation scope of the content. 254 | append Document "\n" 255 | foreach lines $bullets { 256 | append Document "- [my ToMarkdown [join $lines { }] $scope]\n" 257 | } 258 | append Document "\n" 259 | return 260 | } 261 | 262 | method AddPreformattedText {text scope} { 263 | # See [Formatter.AddPreformattedText]. 264 | # text - Preformatted text. 265 | # scope - The documentation scope of the content. 266 | append Document "\n```\n$text\n```\n" 267 | return 268 | } 269 | 270 | method AddFenced {lines fence_options scope} { 271 | # See [Formatter.AddFenced]. 272 | # Adds a list of fenced lines to document content. 273 | # lines - Preformatted text as a list of lines. 274 | # fence_options - options specified with the fence, e.g. diagram ... 275 | # scope - The documentation scope of the content. 276 | # Only obeys -caption option, ignores all else 277 | 278 | # Do not hardcode fence since the lines may themself contain something 279 | # that looks like a fence. 280 | set fence [dict get $fence_options Fence] 281 | set lang [dict get $fence_options Language] 282 | append Document \n $fence$lang \n [join $lines \n] \n $fence \n 283 | if {[dict exists $fence_options -caption]} { 284 | append Document \n\n* [dict get $fence_options -caption] *\n\n 285 | } 286 | 287 | return 288 | } 289 | 290 | method AddSynopsis {synopsis scope} { 291 | # Adds a Synopsis section to the document content. 292 | # synopsis - List of alternating elements comprising the command portion 293 | # and the parameter list for it. 294 | # scope - The documentation scope of the content. 295 | 296 | append Document \n 297 | foreach {cmds params} $synopsis { 298 | append Document "\n> `[join $cmds { }]` *`[join $params { }]`*
" 299 | } 300 | append Document \n 301 | return 302 | } 303 | 304 | method Navigation {{highlight_ns {}}} { 305 | # TBD - right now, no navigation for markdown. 306 | return 307 | } 308 | 309 | method Escape {s} { 310 | # Escapes special characters in markdown. 311 | # s - string to be escaped 312 | # Protects characters in $s against interpretation as 313 | # markdown special characters. 314 | # 315 | # Returns the escaped string 316 | 317 | # TBD - fix this regexp 318 | return [regsub -all {[\\`*_\{\}\[\]\(\)#\+\-\.!<>|]} $s {\\\0}] 319 | } 320 | 321 | # Credits: tcllib/Caius markdown module 322 | method ToMarkdown {text {scope {}}} { 323 | # Returns $text marked up in markdown syntax 324 | # text - Ruff! text with inline markup 325 | # scope - namespace scope to use for symbol lookup 326 | 327 | # We cannot just pass through our marked-up text as is because 328 | # it is not really markdown but rather with some extensions: 329 | # - [xxx] treats xxx as potentially a link to documentation for 330 | # some programming element. 331 | # - _ is not treated as a special char 332 | # - $var is marked as a variable name 333 | # Moreover, we cannot use a simple regexp or subst because 334 | # whether this special processing will depend on where inside 335 | # the input these characters occur, whether a \ preceded etc. 336 | 337 | set text [regsub -all -lineanchor {[ ]{2,}$} $text
] 338 | 339 | set index 0 340 | set result {} 341 | 342 | set re_backticks {\A`+} 343 | set re_whitespace {\s} 344 | set re_inlinelink {\A\!?\[((?:[^\]]|\[[^\]]*?\])+)\]\s*\(\s*((?:[^\s\)]+|\([^\s\)]+\))+)?(\s+([\"'])(.*)?\4)?\s*\)} 345 | # Changed from markdown to require second optional [] to follow first [] 346 | # without any intervening space. This is to allow consecutive symbol references 347 | # not to be interpreted as [ref] [text] instead of [ref] [ref] 348 | # set re_reflink {\A\!?\[((?:[^\]]|\[[^\]]*?\])+)\](?:\s*\[((?:[^\]]|\[[^\]]*?\])*)\])?} 349 | set re_reflink {\A\!?\[((?:[^\]]|\[[^\]]*?\])+)\](?:\[((?:[^\]]|\[[^\]]*?\])*)\])?} 350 | set re_htmltag {\A|\A<\w+(?:\s+\w+=(?:\"[^\"]*\"|\'[^\']*\'))*\s*/?>} 351 | set re_autolink {\A<(?:(\S+@\S+)|(\S+://\S+))>} 352 | set re_comment {\A} 353 | set re_entity {\A\&\S+;} 354 | 355 | while {[set chr [string index $text $index]] ne {}} { 356 | switch $chr { 357 | "\\" { 358 | # If the next character is a special markdown character 359 | # that we do not treat as special, it should be treated 360 | # as a backslash-prefixed ordinary character. 361 | # So double the backslash and prefix the character. 362 | set next_chr [string index $text [expr $index + 1]] 363 | if {$next_chr eq "_"} { 364 | append result "\\\\\\_" 365 | incr index; # Move past \_ 366 | continue 367 | } 368 | # Other characters, special or not, are treated just 369 | # like markdown would so pass through as is at bottom 370 | # of loop. 371 | } 372 | {_} { 373 | # Unlike Markdown, do not treat underscores as special char 374 | append result \\; # Add an escape prefix 375 | # $chr == _ will be added at bottom of loop 376 | } 377 | {*} { 378 | # EMPHASIS 379 | if {[regexp $re_whitespace [string index $result end]] && 380 | [regexp $re_whitespace [string index $text [expr $index + 1]]]} \ 381 | { 382 | #do nothing (add character at bottom of loop) 383 | } \ 384 | elseif {[regexp -start $index \ 385 | "\\A(\\$chr{1,3})((?:\[^\\$chr\\\\]|\\\\\\$chr)*)\\1" \ 386 | $text m del sub]} \ 387 | { 388 | append result "$del[my ToMarkdown $sub $scope]$del" 389 | incr index [string length $m] 390 | continue 391 | } 392 | } 393 | {`} { 394 | # CODE 395 | # Any marked code should not be escaped as above so 396 | # look for it and pass it through as is. 397 | 398 | # Collect the leading backtick sequence 399 | regexp -start $index $re_backticks $text backticks 400 | set start [expr $index + [string length $backticks]] 401 | 402 | # Look for the matching backticks. If not found, 403 | # we will not treat this as code. Otherwise pass through 404 | # the entire match unchanged. 405 | if {[regexp -start $start -indices $backticks $text terminating_indices]} { 406 | set stop [lindex $terminating_indices 1] 407 | # Copy the entire substring including leading and trailing 408 | # backticks to output as is as we do not want those 409 | # characters to undergo the special processing above. 410 | set passthru [string range $text $index $stop] 411 | append result $passthru 412 | incr index [string length $passthru] 413 | continue 414 | } 415 | } 416 | {!} - 417 | {[} { 418 | # LINKS AND IMAGES 419 | if {$chr eq {!}} { 420 | set ref_type img 421 | set pre "!\[" 422 | } else { 423 | set ref_type link 424 | set pre "\[" 425 | } 426 | 427 | set match_found 0 428 | if {[regexp -start $index $re_inlinelink $text m txt url ign del title]} { 429 | # INLINE 430 | if {1} { 431 | append result $m 432 | set match_found 1 433 | } else { 434 | # Note: Do quotes inside $title need to be escaped? 435 | append result $pre [my ToMarkdown $txt $scope] "\](" $url " " "\"[my ToMarkdown $title $scope]\"" ")" 436 | set url [escape [string trim $url {<> }]] 437 | set match_found 1 438 | } 439 | } elseif {[regexp -start $index $re_reflink $text m txt lbl]} { 440 | if {$lbl eq {}} { 441 | set lbl [regsub -all {\s+} $txt { }] 442 | set display_text_specified 0 443 | } else { 444 | set display_text_specified 1 445 | } 446 | 447 | if {[my ResolvableReference? $lbl $scope code_link]} { 448 | # RUFF CODE REFERENCE 449 | set url [my Escape [dict get $code_link ref]] 450 | if {! $display_text_specified} { 451 | set txt [my Escape [dict get $code_link label]] 452 | } 453 | if {1} { 454 | append result $pre $txt "\](" $url ")" 455 | } else { 456 | # Note: Do quotes inside $txt (SECOND occurence) need to be escaped? 457 | append result $pre $txt "\](" $url " " "\"$txt\"" ")" 458 | } 459 | set match_found 1 460 | } else { 461 | # Not a Ruff! code link. Pass through as is. 462 | # We do not pass text through ToMarkdown as it is 463 | # treated as a markdown reference and will need 464 | # to match the reference entry. 465 | app::log_error "Warning: no target found for link \"$lbl\". Assuming markdown reference." 466 | append result $m 467 | set match_found 1 468 | } 469 | } 470 | # PRINT IMG, A TAG 471 | if {$match_found} { 472 | incr index [string length $m] 473 | continue 474 | } 475 | } 476 | {<} { 477 | # HTML TAGS, COMMENTS AND AUTOLINKS 478 | # HTML tags, pass through as is without processing 479 | if {[regexp -start $index $re_comment $text m] || 480 | [regexp -start $index $re_autolink $text m email link] || 481 | [regexp -start $index $re_htmltag $text m]} { 482 | append result $m 483 | incr index [string length $m] 484 | continue 485 | } 486 | # Else fall through to pass only the < character 487 | } 488 | {&} { 489 | # ENTITIES 490 | # Pass through entire entity without processing 491 | if {[regexp -start $index $re_entity $text m]} { 492 | append result $m 493 | incr index [string length $m] 494 | continue 495 | } 496 | # Else fall through to processing this single & 497 | } 498 | {$} { 499 | # Ruff extension - treat $var as variables name 500 | # Note: no need to escape characters but do so 501 | # if you change the regexp below 502 | if {[regexp -start $index {\$\w+} $text m]} { 503 | append result "`$m`" 504 | incr index [string length $m] 505 | continue 506 | } 507 | } 508 | {>} - 509 | {'} - 510 | "\"" { 511 | # OTHER SPECIAL CHARACTERS 512 | # Pass through 513 | } 514 | default {} 515 | } 516 | 517 | append result $chr 518 | incr index 519 | } 520 | 521 | return $result 522 | } 523 | 524 | method extension {} { 525 | # Returns the default file extension to be used for output files. 526 | return md 527 | } 528 | 529 | forward FormatInline my ToMarkdown 530 | } 531 | -------------------------------------------------------------------------------- /src/formatter_nroff.tcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Ashok P. Nadkarni 2 | # All rights reserved. 3 | # See the file LICENSE in the source root directory for license. 4 | 5 | # Ruff! formatter for nroff 6 | 7 | namespace eval ruff::formatter {} 8 | 9 | oo::class create ruff::formatter::Nroff { 10 | superclass ::ruff::formatter::Formatter 11 | 12 | # Data members 13 | variable DocumentNamespace; # Namespace being documented 14 | variable Header; # Common header for all pages 15 | variable PageTitle; # Page title - first line of man page 16 | variable Synopsis; # Holds the synopsis 17 | variable Body; # Main content 18 | variable Footer; # Common footer 19 | variable HeaderLevels; # Header levels for various headers 20 | variable SeeAlso; # The See also section 21 | 22 | variable Indentation; # How much to indent in nroff units 23 | 24 | constructor args { 25 | namespace path [linsert [namespace path] 0 ::ruff::formatter::nroff] 26 | set HeaderLevels { 27 | class 3 28 | proc 4 29 | method 4 30 | nonav 5 31 | parameters 5 32 | } 33 | set Indentation 4n 34 | next {*}$args 35 | } 36 | 37 | method CollectReferences args {} 38 | method CollectHeadingReference args {} 39 | method CollectFigureReference args {} 40 | export CollectFigureReference 41 | 42 | method Begin {} { 43 | # Implements the [Formatter.Begin] method for nroff. 44 | 45 | next 46 | 47 | # Generate the header used by all files 48 | # Currently, it is empty but might change in the future with 49 | # support for specific dialects which implement metainformation. 50 | set Header "" 51 | 52 | append Header [nr_comment "\n"] 53 | if {[my Option? -copyright copyright]} { 54 | append Header [nr_comment "Copyright (c) [my Escape $copyright]\n"] 55 | } 56 | 57 | # Generate the Footer used by all files 58 | set Footer "" 59 | return 60 | } 61 | 62 | method DocumentBegin {ns} { 63 | # See [Formatter.DocumentBegin]. 64 | # ns - Namespace for this document. 65 | set ns [string trimleft $ns :] 66 | 67 | next $ns 68 | 69 | set DocumentNamespace $ns 70 | set Body "" 71 | set Synopsis "" 72 | set SeeAlso "" 73 | 74 | return 75 | } 76 | 77 | method DocumentEnd {} { 78 | # See [Formatter.DocumentEnd]. 79 | 80 | set title [my Option -title] 81 | set section [my Option -section 3tcl] 82 | set version [my Option -version 0.0] 83 | set product [my Option -product $DocumentNamespace] 84 | if {$DocumentNamespace eq ""} { 85 | set header_left $product 86 | } else { 87 | set header_left $DocumentNamespace 88 | } 89 | set PageTitle [nr_title "\"$header_left\" $section $version \"$product\" \"$title\""] 90 | 91 | # Add the navigation bits and footer 92 | append doc $Header $PageTitle 93 | append doc [nr_section NAME] \n 94 | if {$DocumentNamespace eq ""} { 95 | append doc "Introduction - $title" 96 | } else { 97 | append doc "$DocumentNamespace - Commands in namespace $DocumentNamespace" 98 | } 99 | if {$Synopsis ne ""} { 100 | append doc [nr_section SYNOPSIS] \n $Synopsis 101 | } 102 | append doc $Body 103 | if {$SeeAlso ne ""} { 104 | append doc [nr_section "SEE ALSO"] $SeeAlso 105 | } 106 | append doc $Footer 107 | set doc [nroff_postprocess $doc] 108 | 109 | next 110 | 111 | return $doc 112 | } 113 | 114 | method AddProgramElementHeading {type fqn {tooltip {}} {synopsis {}}} { 115 | # Adds heading for a program element like procedure, class or method. 116 | # type - One of `proc`, `class` or `method` 117 | # fqn - Fully qualified name of element. 118 | # tooltip - The tooltip lines, if any. Ignore for nroff output. 119 | set level [dict get $HeaderLevels $type] 120 | set ns [namespace qualifiers $fqn] 121 | set name [namespace tail $fqn] 122 | append Body [nr_p] [nr_inn -$Indentation] \n [nr_bldr $name] 123 | if {[string length $ns]} { 124 | append Body " ($ns)" 125 | #append Body [nr_p] [nr_inn -$Indentation] \n [nr_bldr $name] " ($ns)" 126 | } else { 127 | #append Body [nr_p] [nr_inn -$Indentation] \n [nr_bldr $name] 128 | } 129 | append Body [nr_out] 130 | 131 | if {[llength $synopsis]} { 132 | foreach {cmds params} $synopsis { 133 | set line "[nr_bldp [join $cmds { }]]" 134 | if {[llength $params]} { 135 | append line " " [nr_ulp [join $params { }]] 136 | } 137 | append Synopsis $line [nr_br] 138 | } 139 | } elseif {$type eq "class"} { 140 | # Classes without constructors will not have a synopsis 141 | # Make one up 142 | if {[llength [info class constructor $fqn]] == 0} { 143 | append Synopsis [nr_bldp "$name create OBJNAME"] [nr_br] 144 | append Synopsis [nr_bldp "$name new"] [nr_br] 145 | } 146 | } 147 | return 148 | } 149 | 150 | method AddHeading {level text scope {tooltip {}}} { 151 | # See [Formatter.AddHeading]. 152 | # level - The numeric or semantic heading level. 153 | # text - The heading text. 154 | # scope - The documentation scope of the content. 155 | # tooltip - Tooltip to display in navigation link. 156 | 157 | if {![string is integer -strict $level]} { 158 | set level [dict get $HeaderLevels $level] 159 | } 160 | 161 | # TBD - should $text really be passed through ToNroff? In particular do 162 | # commands like .SH accept embedded escapes ? 163 | set text [my ToNroff $text $scope] 164 | if {$level < 3} { 165 | append Body [nr_section $text] 166 | } elseif {$level == 3} { 167 | append Body [nr_subsection $text] 168 | } else { 169 | append Body [nr_p] [nr_bldr $text] 170 | } 171 | return 172 | } 173 | 174 | method AddParagraph {lines scope} { 175 | # See [Formatter.AddParagraph]. 176 | # lines - The paragraph lines. 177 | # scope - The documentation scope of the content. 178 | append Body [nr_p] [my ToNroff [join $lines \n] $scope] 179 | return 180 | } 181 | 182 | method AddDefinitions {definitions scope {preformatted none}} { 183 | # See [Formatter.AddDefinitions]. 184 | # definitions - List of definitions. 185 | # scope - The documentation scope of the content. 186 | # preformatted - One of `none`, `both`, `term` or `definition` 187 | # indicating which fields of the definition are 188 | # are already formatted. 189 | 190 | # Note: CommonMark does not recognize tables without a heading line 191 | # TBD - how do empty headers look in generated HTML? 192 | set autopunctuate [my Option -autopunctuate 0] 193 | append Body [nr_inn $Indentation] 194 | foreach item $definitions { 195 | set def [join [dict get $item definition] " "] 196 | if {$autopunctuate} { 197 | set def [string toupper $def 0 0] 198 | if {[regexp {[[:alnum:]]} [string index $def end]]} { 199 | append def "." 200 | } 201 | } 202 | if {$preformatted in {none term}} { 203 | set def [my ToNroff $def $scope] 204 | } 205 | set term [dict get $item term] 206 | if {$preformatted in {none definition}} { 207 | set term [my ToNroff $term $scope] 208 | } 209 | append Body [nr_blt $term] "\n" $def 210 | } 211 | append Body [nr_out] 212 | 213 | return 214 | } 215 | 216 | method AddBullets {bullets scope} { 217 | # See [Formatter.AddBullets]. 218 | # bullets - The list of bullets. 219 | # scope - The documentation scope of the content. 220 | foreach lines $bullets { 221 | append Body [nr_blt "\n\1\\(bu"] "\n" [my ToNroff [join $lines { }] $scope] 222 | } 223 | return 224 | } 225 | 226 | method AddPreformattedText {text scope} { 227 | # See [Formatter.AddPreformattedText]. 228 | # text - Preformatted text. 229 | # scope - The documentation scope of the content. 230 | 231 | append Body [nr_p] [nr_inn $Indentation] [nr_nofill] \n 232 | append Body $text 233 | append Body [nr_fill] [nr_out] 234 | return 235 | } 236 | 237 | method AddFenced {lines fence_options scope} { 238 | # See [Formatter.AddFenced]. 239 | # Adds a list of fenced lines to document content. 240 | # lines - Preformatted text as a list of lines. 241 | # fence_options - options specified with the fence, e.g. diagram ... 242 | # scope - The documentation scope of the content. 243 | # Only obeys -caption option, ignores all else 244 | append Body [nr_p] [nr_inn $Indentation] [nr_nofill] \n 245 | append Body [join $lines \n] 246 | if {[dict exists $fence_options -caption]} { 247 | append Body \n\n [nr_ulp [dict get $fence_options -caption]] \n 248 | } 249 | append Body [nr_fill] [nr_out] 250 | return 251 | } 252 | 253 | method AddSynopsis {synopsis scope} { 254 | # Adds a synopsis for a command 255 | # synopsis - List of alternating elements comprising the command portion 256 | # and the parameter list for it. 257 | # scope - The documentation scope of the content. 258 | 259 | # Do not confuse this synopsis with the synopsis section! 260 | 261 | append Body [nr_inn $Indentation] \n; # Indent the synopsis 262 | foreach {cmds params} $synopsis { 263 | set line "[nr_bldp [join $cmds { }]]" 264 | if {[llength $params]} { 265 | append line " " [nr_ulp [join $params { }]] 266 | } 267 | append Body $line [nr_br] 268 | } 269 | append Body [nr_out] \n 270 | return 271 | } 272 | 273 | method Navigation {{highlight_ns {}}} { 274 | return 275 | } 276 | 277 | method Escape {s} { 278 | # Escapes special characters in nroff. 279 | # s - string to be escaped 280 | # Protects characters in $s against interpretation as 281 | # nroff special characters. 282 | # 283 | # Returns the escaped string 284 | 285 | # TBD - fix this? 286 | return [string map [list \\ \\\\] $s] 287 | } 288 | 289 | # Credits: tcllib/Caius markdown module 290 | method ToNroff {text {scope {}}} { 291 | # Returns $text marked up in nroff syntax 292 | # text - Ruff! text with inline markup 293 | # scope - namespace scope to use for symbol lookup 294 | 295 | # Passed in text is kinda markdown but with some extensions: 296 | # - [xxx] treats xxx as potentially a link to documentation for 297 | # some programming element. 298 | # - _ is not treated as a special char 299 | # - $var is marked as a variable name 300 | # Moreover, we cannot use a simple regexp or subst because 301 | # whether this special processing will depend on where inside 302 | # the input these characters occur, whether a \ preceded etc. 303 | 304 | set text [regsub -all -lineanchor {[ ]{2,}$} $text [nr_br]] 305 | 306 | set index 0 307 | set result {} 308 | 309 | set re_backticks {\A`+} 310 | set re_whitespace {\s} 311 | set re_inlinelink {\A\!?\[((?:[^\]]|\[[^\]]*?\])+)\]\s*\(\s*((?:[^\s\)]+|\([^\s\)]+\))+)?(\s+([\"'])(.*)?\4)?\s*\)} 312 | # Changed from markdown to require second optional [] to follow first [] 313 | # without any intervening space. This is to allow consecutive symbol references 314 | # not to be interpreted as [ref] [text] instead of [ref] [ref] 315 | # set re_reflink {\A\!?\[((?:[^\]]|\[[^\]]*?\])+)\](?:\s*\[((?:[^\]]|\[[^\]]*?\])*)\])?} 316 | set re_reflink {\A\!?\[((?:[^\]]|\[[^\]]*?\])+)\](?:\[((?:[^\]]|\[[^\]]*?\])*)\])?} 317 | set re_htmltag {\A|\A<\w+(?:\s+\w+=(?:\"[^\"]*\"|\'[^\']*\'))*\s*/?>} 318 | set re_autolink {\A<(?:(\S+@\S+)|(\S+://\S+))>} 319 | set re_comment {\A} 320 | set re_entity {\A\&\S+;} 321 | 322 | while {[set chr [string index $text $index]] ne {}} { 323 | switch $chr { 324 | "\\" { 325 | # If next character is a special markdown char, set that as the 326 | # the character. Otherwise just pass this \ as the character. 327 | set next_chr [string index $text [expr $index + 1]] 328 | if {[string first $next_chr {\`*_\{\}[]()#+-.!>|}] != -1} { 329 | set chr $next_chr 330 | incr index 331 | } 332 | } 333 | {_} { 334 | # Unlike Markdown, underscores are not treated as special char 335 | } 336 | {*} { 337 | # EMPHASIS 338 | if {[regexp $re_whitespace [string index $result end]] && 339 | [regexp $re_whitespace [string index $text [expr $index + 1]]]} \ 340 | { 341 | #do nothing (add character at bottom of loop) 342 | } elseif {[regexp -start $index \ 343 | "\\A(\\$chr{1,3})((?:\[^\\$chr\\\\]|\\\\\\$chr)*)\\1" \ 344 | $text m del sub]} { 345 | switch [string length $del] { 346 | 1 { 347 | # * - Emphasis 348 | append result "[nr_ul][my ToNroff $sub $scope][nr_fpop]" 349 | } 350 | 2 { 351 | # ** - Strong 352 | append result "[nr_bld][my ToNroff $sub $scope][nr_fpop]" 353 | } 354 | 3 { 355 | # *** - Strong+emphasis - no way I think. Make bold 356 | append result "[nr_bld][my ToNroff $sub $scope][nr_fpop]" 357 | } 358 | } 359 | 360 | incr index [string length $m] 361 | continue 362 | } 363 | } 364 | {`} { 365 | # CODE 366 | # Any marked code should not be escaped as above so 367 | # look for it and pass it through as is. 368 | # TBD - anything needed to pass text verbatim? 369 | 370 | # Collect the leading backtick sequence 371 | regexp -start $index $re_backticks $text backticks 372 | set start [expr $index + [string length $backticks]] 373 | 374 | # Look for the matching backticks. If not found, 375 | # we will not treat this as code. Otherwise pass through 376 | # the entire match unchanged. 377 | if {[regexp -start $start -indices $backticks $text terminating_indices]} { 378 | set stop [expr {[lindex $terminating_indices 0] - 1}] 379 | 380 | set sub [string trim [string range $text $start $stop]] 381 | 382 | append result "[my Escape $sub]" 383 | set index [expr [lindex $terminating_indices 1] + 1] 384 | continue 385 | } 386 | } 387 | {!} - 388 | "\[" { 389 | # LINKS AND IMAGES 390 | if {$chr eq {!}} { 391 | set ref_type img 392 | set pre "!\[" 393 | } else { 394 | set ref_type link 395 | set pre "\[" 396 | } 397 | 398 | set match_found 0 399 | if {[regexp -start $index $re_inlinelink $text m txt url ign del title]} { 400 | # INLINE 401 | incr index [string length $m] 402 | 403 | set url [my Escape [string trim $url {<> }]] 404 | set txt [my ToNroff $txt $scope] 405 | set title [my ToNroff $title $scope] 406 | 407 | set match_found 1 408 | } elseif {[regexp -start $index $re_reflink $text m txt lbl]} { 409 | if {$lbl eq {}} { 410 | # Be loose in whitespace 411 | set lbl [regsub -all {\s+} $txt { }] 412 | set display_text_specified 0 413 | } else { 414 | set display_text_specified 1 415 | } 416 | 417 | set code_link "" 418 | if {[my ResolvableReference? $lbl $scope code_link]} { 419 | # RUFF CODE REFERENCE 420 | set url [my Escape [dict get $code_link ref]] 421 | } else { 422 | set url "" 423 | } 424 | if {! $display_text_specified && $code_link ne ""} { 425 | set txt [my Escape [dict get $code_link label]] 426 | } 427 | set title $txt 428 | incr index [string length $m] 429 | set match_found 1 430 | } 431 | # PRINT IMG, A TAG 432 | if {$match_found} { 433 | if {$ref_type eq {link}} { 434 | # TBD - some nroff version support urls using .UR 435 | append result [nr_ulr $txt] 436 | if {$url ne ""} { 437 | append result " \[URL: $url\]" 438 | } 439 | } else { 440 | app::log_error "Warning: Image URL $url found. Images are not supported for Nroff output." 441 | append result $txt " \[Image: $url\]" 442 | } 443 | 444 | continue 445 | } 446 | } 447 | {<} { 448 | # HTML TAGS, COMMENTS AND AUTOLINKS 449 | # HTML tags, pass through as is without processing 450 | 451 | if {[regexp -start $index $re_comment $text m]} { 452 | append result [nr_comment [string range $m 4 end-3]] 453 | incr index [string length $m] 454 | continue 455 | } elseif {[regexp -start $index $re_autolink $text m email link]} { 456 | if {$link ne {}} { 457 | set link [my Escape $link] 458 | append result " \[URL: $link\]" 459 | } else { 460 | set mailto_prefix "mailto:" 461 | if {![regexp "^${mailto_prefix}(.*)" $email mailto email]} { 462 | # $email does not contain the prefix "mailto:". 463 | set mailto "mailto:$email" 464 | } 465 | append result "$email" 466 | append result " \[$mailto\]" 467 | } 468 | incr index [string length $m] 469 | continue 470 | } elseif {[regexp -start $index $re_htmltag $text m]} { 471 | app::log_error "Warning: HTML tag $m skipped. HTML tags not supported by Nroff formatter." 472 | incr index [string length $m] 473 | continue 474 | } 475 | # Else fall through to pass only the < character 476 | } 477 | {&} { 478 | # ENTITIES 479 | # Pass through entire entity without processing 480 | # TBD - add support for more entities 481 | if {[regexp -start $index $re_entity $text m]} { 482 | set html_mapping [list """ \" "'" ' "&" & "<" < ">" >] 483 | append result [string map $html_mapping $m] 484 | incr index [string length $m] 485 | continue 486 | } 487 | # Else fall through to processing this single & 488 | } 489 | {$} { 490 | # Ruff extension - treat $var as variables name 491 | # Note: no need to escape characters but do so 492 | # if you change the regexp below 493 | if {[regexp -start $index {\$\w+} $text m]} { 494 | append result [nr_ulr $m] 495 | incr index [string length $m] 496 | continue 497 | } 498 | } 499 | {>} - 500 | {'} - 501 | "\"" { 502 | # OTHER SPECIAL CHARACTERS 503 | # Pass through 504 | } 505 | default {} 506 | } 507 | 508 | append result $chr 509 | incr index 510 | } 511 | 512 | return $result 513 | } 514 | 515 | method extension {} { 516 | # Returns the default file extension to be used for output files. 517 | return 3tcl 518 | } 519 | 520 | forward FormatInline my ToNroff 521 | } 522 | 523 | # MODFIED/ADAPTED From tcllib - BSD license.] 524 | namespace eval ruff::formatter::nroff { 525 | # -*- tcl -*- 526 | # 527 | # -- nroff commands 528 | # 529 | # Copyright (c) 2003-2019 Andreas Kupries 530 | 531 | ################################################################ 532 | # nroff specific commands 533 | # 534 | # All dot-commands (f.e. .PP) are returned with a leading \n\1, 535 | # enforcing that they are on a new line and will be protected as markup. 536 | # Any empty line created because of this is filtered out in the 537 | # post-processing step. 538 | 539 | 540 | proc nr_lp {} {return \n\1.LP} 541 | proc nr_ta {{text {}}} {return "\n\1.ta$text"} 542 | proc nr_bld {} {return \1\\fB} 543 | proc nr_bldt {t} {return "\n\1.B $t\n"} 544 | proc nr_bldr {t} {return \1\\fB$t[nr_rst]} 545 | proc nr_bldp {t} {return \1\\fB$t[nr_fpop]} 546 | proc nr_ul {} {return \1\\fI} 547 | proc nr_ulr {t} {return \1\\fI$t[nr_fpop]} 548 | proc nr_ulp {t} {return \1\\fI$t[nr_fpop]} 549 | proc nr_rst {} {return \1\\fR} 550 | proc nr_fpop {} {return \1\\fP} 551 | proc nr_p {} {return \n\1.PP\n} 552 | proc nr_comment {text} {return "\1'\1\\\" [join [split $text \n] "\n\1'\1\\\" "]"} ; # " 553 | proc nr_enum {num} {nr_item " \[$num\]"} 554 | proc nr_item {{text {}}} {return "\n\1.IP$text"} 555 | proc nr_vspace {} {return \n\1.sp\n} 556 | proc nr_br {} {return \n\1.br\n} 557 | proc nr_blt {text} {return "\n\1.TP\n$text"} 558 | proc nr_bltn {n text} {return "\n\1.TP $n\n$text"} 559 | proc nr_in {} {return \n\1.RS} 560 | proc nr_inn {n} {return "\n\1.RS $n"} 561 | proc nr_out {} {return \n\1.RE} 562 | proc nr_nofill {} {return \n\1.nf} 563 | proc nr_fill {} {return \n\1.fi} 564 | proc nr_title {text} {return "\n\1.TH $text"} 565 | proc nr_include {file} {return "\n\1.so $file"} 566 | proc nr_bolds {} {return \n\1.BS} 567 | proc nr_bolde {} {return \n\1.BE} 568 | proc nr_read {fn} {return [nroffMarkup [dt_read $fn]]} 569 | proc nr_cs {} {return \n\1.CS\n} 570 | proc nr_ce {} {return \n\1.CE\n} 571 | 572 | proc nr_section {name} { 573 | if {![regexp {[ ]} $name]} { 574 | return "\n\1.SH [string toupper $name]" 575 | } 576 | return "\n\1.SH \"[string toupper $name]\"" 577 | } 578 | proc nr_subsection {name} { 579 | if {![regexp {[ ]} $name]} { 580 | return "\n\1.SS [string toupper $name]" 581 | } 582 | return "\n\1.SS \"[string toupper $name]\"" 583 | } 584 | 585 | 586 | ################################################################ 587 | 588 | # Handling of nroff special characters in content: 589 | # 590 | # Plain text is initially passed through unescaped; 591 | # internally-generated markup is protected by preceding it with \1. 592 | # The final PostProcess step strips the escape character from 593 | # real markup and replaces unadorned special characters in content 594 | # with proper escapes. 595 | # 596 | 597 | variable markupMap 598 | set markupMap [list \ 599 | "\\" "\1\\" \ 600 | "'" "\1'" \ 601 | "." "\1." \ 602 | "\\\\" "\\"] 603 | variable finalMap 604 | set finalMap [list \ 605 | "\1\\" "\\" \ 606 | "\1'" "'" \ 607 | "\1." "." \ 608 | "." "\\&." \ 609 | "\\" "\\\\"] 610 | variable textMap 611 | set textMap [list "\\" "\\\\"] 612 | 613 | 614 | proc nroffEscape {text} { 615 | variable textMap 616 | return [string map $textMap $text] 617 | } 618 | 619 | # markup text -- 620 | # Protect markup characters in $text. 621 | # These will be stripped out in PostProcess. 622 | # 623 | proc nroffMarkup {text} { 624 | variable markupMap 625 | return [string map $markupMap $text] 626 | } 627 | 628 | proc nroff_postprocess {nroff} { 629 | variable finalMap 630 | 631 | # Postprocessing final nroff text. 632 | # - Strip empty lines out of the text 633 | # - Remove leading and trailing whitespace from lines. 634 | # - Exceptions to the above: Keep empty lines and leading 635 | # whitespace when in verbatim sections (no-fill-mode) 636 | 637 | set nfMode [list \1.nf \1.CS] ; # commands which start no-fill mode 638 | set fiMode [list \1.fi \1.CE] ; # commands which terminate no-fill mode 639 | set lines [list] ; # Result buffer 640 | set verbatim 0 ; # Automaton mode/state 641 | 642 | foreach line [split $nroff "\n"] { 643 | #puts_stderr |[expr {$verbatim ? "VERB" : " "}]|$line| 644 | 645 | if {!$verbatim} { 646 | # Normal lines, not in no-fill mode. 647 | 648 | if {[lsearch -exact $nfMode [split $line]] >= 0} { 649 | # no-fill mode starts after this line. 650 | set verbatim 1 651 | } 652 | 653 | # Ensure that empty lines are not added. 654 | # This also removes leading and trailing whitespace. 655 | 656 | if {![string length $line]} {continue} 657 | set line [string trim $line] 658 | if {![string length $line]} {continue} 659 | 660 | if {[regexp {^\x1\\f[BI]\.} $line]} { 661 | # We found confusing formatting at the beginning of 662 | # the current line. We lift this line up and attach it 663 | # at the end of the last line to remove this 664 | # irregularity. Note that the regexp has to look for 665 | # the special 0x01 character as well to be sure that 666 | # the sequence in question truly is formatting. 667 | # [bug-3601370] Only lift & attach if last line is not 668 | # a directive 669 | 670 | set last [lindex $lines end] 671 | if { ! [string match "\1.*" $last] } { 672 | #puts_stderr \tLIFT 673 | set lines [lreplace $lines end end] 674 | set line "$last $line" 675 | } 676 | } elseif {[string match {[']*} $line]} { 677 | # Apostrophes at the beginning of a line have to be 678 | # quoted to prevent misinterpretation as comments. 679 | # The true comments and are quoted with \1 already and 680 | # will therefore not detected by the code here. 681 | # puts_stderr \tQUOTE 682 | set line \1\\$line 683 | } ; # We are not handling dots at the beginning of a line here. 684 | # # We are handling them in the finalMap which will quote _all_ 685 | # # dots in a text with a zero-width escape (\&). 686 | } else { 687 | # No-fill mode. We remove trailing whitespace, but keep 688 | # leading whitespace and empty lines. 689 | 690 | if {[lsearch -exact $fiMode [split $line]] >= 0} { 691 | # Normal mode resumes after this line. 692 | set verbatim 0 693 | } 694 | set line [string trimright $line] 695 | } 696 | lappend lines $line 697 | } 698 | 699 | set lines [join $lines "\n"] 700 | 701 | # Now remove all superfluous .IP commands (empty paragraphs). The 702 | # first identity mapping is present to avoid smashing a man macro 703 | # definition. 704 | 705 | lappend map \n\1.IP\n\1.\1.\n \n\1.IP\n\1.\1.\n 706 | lappend map \n\1.IP\n\1. \n\1. 707 | 708 | set lines [string map $map $lines] 709 | 710 | # Return the modified result buffer 711 | return [string trim [string map $finalMap $lines]]\n 712 | } 713 | } 714 | -------------------------------------------------------------------------------- /src/makedist.tcl: -------------------------------------------------------------------------------- 1 | source [file join [file dirname [file dirname [file normalize [info script]]]] src ruff.tcl] 2 | 3 | proc ruff::private::distribute {{dir {}}} { 4 | 5 | if {$dir eq ""} { 6 | set dir [file join [file dirname [ruff_dir]] dist] 7 | } 8 | set outname ruff-[version] 9 | set dir [file join $dir $outname] 10 | file delete -force $dir; # Empty it out 11 | file mkdir $dir 12 | set files { 13 | pkgIndex.tcl 14 | ruff.tcl 15 | formatter.tcl 16 | formatter_html.tcl 17 | formatter_markdown.tcl 18 | formatter_nroff.tcl 19 | diagram.tcl 20 | ../doc/sample.tcl 21 | ../doc/html/ruff.html 22 | ../doc/html/ruff-ruff.html 23 | ../doc/html/ruff-ruff-sample.html 24 | ../LICENSE 25 | ../README.md 26 | } 27 | file copy -force -- {*}[lmap file $files {file join [ruff_dir] $file}] $dir 28 | file copy -force -- [file join [ruff_dir] msgs] $dir 29 | 30 | # Copy assets 31 | # Ensure minimized versions are up to date 32 | set assets_dir [file join $dir assets] 33 | file mkdir $assets_dir 34 | foreach {max min} { 35 | ruff.css ruff-min.css 36 | ruff.js ruff-min.js 37 | ruff-index.js ruff-index-min.js 38 | } { 39 | # Minimization: csso -i ruff-html.css -o ruff-html-min.css 40 | # Minimization: uglifyjs ruff.js -b beautify=false -b ascii_only=true -o ruff-min.js 41 | set max [file join [ruff_dir] assets $max] 42 | set min [file join [ruff_dir] assets $min] 43 | if {[file mtime $max] > [file mtime $min]} { 44 | app::log_error "File $max is newer than $min. Please regenerate $min." 45 | exit 1 46 | } 47 | file copy -force -- $min $assets_dir 48 | } 49 | file copy -force [file join [ruff_dir] assets ruff-md.css] $assets_dir 50 | file copy -force [file join [ruff_dir] assets ruff-logo.png] $assets_dir 51 | 52 | # Zip it all 53 | set zipfile [file join $dir ${outname}.zip] 54 | file delete -force -- $zipfile 55 | set curdir [pwd] 56 | try { 57 | cd [file join $dir ..] 58 | exec {*}[auto_execok zip.exe] -r ${outname}.zip $outname 59 | } finally { 60 | cd $curdir 61 | } 62 | } 63 | 64 | if {[catch { 65 | ruff::private::distribute {*}$argv 66 | } result]} { 67 | puts stderr $result 68 | } else { 69 | puts stdout $result 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/makedocs.tcl: -------------------------------------------------------------------------------- 1 | source [file join [file dirname [file dirname [file normalize [info script]]]] src ruff.tcl] 2 | 3 | proc ruff::private::document_self {args} { 4 | # Generates documentation for Ruff! 5 | # -formatter FORMATTER - the formatter to use (default html) 6 | # -outdir DIRPATH - the output directory where files will be stored. Note 7 | # files in this directory with the same name as the output files 8 | # will be overwritten! (default sibling `doc` directory) 9 | # -includesource BOOLEAN - if `true`, include source code in documentation. 10 | # Default is `false`. 11 | 12 | variable ruff_dir 13 | variable names 14 | 15 | array set opts [list \ 16 | -format html \ 17 | -includesource true \ 18 | -pagesplit namespace \ 19 | -makeindex true \ 20 | -includeprivate false \ 21 | -compact 1 \ 22 | -locale en \ 23 | -autopunctuate true \ 24 | -navigation {left sticky} 25 | ] 26 | array set opts $args 27 | if {![info exists opts(-outdir)]} { 28 | set opts(-outdir) [file join [file dirname [ruff_dir]] doc $opts(-format)] 29 | } 30 | 31 | if {![namespace exists ::ruff::sample]} { 32 | uplevel #0 [list source [file join [file dirname [ruff_dir]] doc sample.tcl]] 33 | } 34 | 35 | load_formatters 36 | 37 | file mkdir [file join $opts(-outdir) assets] 38 | file copy -force [file join [ruff_dir] assets ruff-logo.png] [file join $opts(-outdir) assets ruff-logo.png] 39 | 40 | set namespaces [list ::ruff ::ruff::app ::ruff::sample] 41 | set common_args [list \ 42 | -outdir $opts(-outdir) \ 43 | -compact $opts(-compact) \ 44 | -format $opts(-format) \ 45 | -recurse $opts(-includeprivate) \ 46 | -makeindex $opts(-makeindex) \ 47 | -pagesplit $opts(-pagesplit) \ 48 | -preamble $::ruff::_ruff_intro \ 49 | -autopunctuate $opts(-autopunctuate) \ 50 | -locale $opts(-locale) \ 51 | -product "Ruff!" \ 52 | -version $::ruff::version] 53 | if {$opts(-includeprivate)} { 54 | lappend common_args -recurse 1 -includeprivate 1 55 | } else { 56 | lappend common_args -excludeprocs {^[_A-Z]} 57 | } 58 | switch -exact -- $opts(-format) { 59 | markdown { 60 | document $namespaces {*}$common_args \ 61 | -outdir $opts(-outdir) \ 62 | -copyright "[clock format [clock seconds] -format %Y] Ashok P. Nadkarni" \ 63 | -includesource $opts(-includesource) 64 | } 65 | html { 66 | if {[info exists opts(-navigation)]} { 67 | if {"fixed" in $opts(-navigation)} { 68 | app::log_error "Warning: \"fixed\" navigation pane option no longer supported. Falling back to \"sticky\"." 69 | set opts(-navigation) sticky 70 | } 71 | lappend common_args -navigation $opts(-navigation) 72 | } 73 | document $namespaces {*}$common_args \ 74 | -outdir $opts(-outdir) \ 75 | -copyright "[clock format [clock seconds] -format %Y] Ashok P. Nadkarni" \ 76 | -includesource $opts(-includesource) 77 | } 78 | nroff { 79 | document $namespaces {*}$common_args \ 80 | -outdir $opts(-outdir) \ 81 | -copyright "[clock format [clock seconds] -format %Y] Ashok P. Nadkarni" \ 82 | -includesource $opts(-includesource) 83 | } 84 | default { 85 | # The formatter may exist but we do not support it for 86 | # out documentation. 87 | error "Format '$opts(-format)' not implemented for generating Ruff! documentation." 88 | } 89 | } 90 | return 91 | } 92 | 93 | 94 | if {[catch { 95 | ruff::private::document_self -format nroff {*}$argv 96 | #ruff::private::document_self -format html {*}$argv 97 | #ruff::private::document_self -format markdown {*}$argv 98 | } result edict]} { 99 | puts stderr "Error: $result" 100 | puts [dict get $edict -errorinfo] 101 | } else { 102 | if {$result ne ""} { 103 | puts stdout $result 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /src/msgs/de.msg: -------------------------------------------------------------------------------- 1 | # UTF-8 encoding! 2 | 3 | ::msgcat::mcflmset { 4 | Commands Befehle 5 | Classes Klassen 6 | Parameters Parameter 7 | Description Beschreibung 8 | {Return value} Rückgabewert 9 | {See also} {Siehe auch} 10 | } 11 | -------------------------------------------------------------------------------- /src/pkgIndex.tcl: -------------------------------------------------------------------------------- 1 | # If you change version here, change in ruff.tcl as well 2 | package ifneeded ruff 2.5.0 [list source [file join $dir ruff.tcl]] 3 | -------------------------------------------------------------------------------- /src/ruff-yui.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2009, Yahoo! Inc. All rights reserved. 3 | Code licensed under the BSD License: 4 | http://developer.yahoo.net/yui/license.txt 5 | version: 2.7.0 6 | */ 7 | html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{*font-size:100%;}body{font:13px/1.231 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;}select,input,button,textarea,button{font:99% arial,helvetica,clean,sans-serif;}table{font-size:inherit;font:100%;}pre,code,kbd,samp,tt{font-family:monospace;*font-size:108%;line-height:100%;}body{text-align:center;}#doc,#doc2,#doc3,#doc4,.yui-t1,.yui-t2,.yui-t3,.yui-t4,.yui-t5,.yui-t6,.yui-t7{margin:auto;text-align:left;width:57.69em;*width:56.25em;}#doc2{width:73.076em;*width:71.25em;}#doc3{margin:auto 10px;width:auto;}#doc4{width:74.923em;*width:73.05em;}.yui-b{position:relative;}.yui-b{_position:static;}#yui-main .yui-b{position:static;}#yui-main,.yui-g .yui-u .yui-g{width:100%;}.yui-t1 #yui-main,.yui-t2 #yui-main,.yui-t3 #yui-main{float:right;margin-left:-25em;}.yui-t4 #yui-main,.yui-t5 #yui-main,.yui-t6 #yui-main{float:left;margin-right:-25em;}.yui-t1 .yui-b{float:left;width:12.30769em;*width:12.00em;}.yui-t1 #yui-main .yui-b{margin-left:13.30769em;*margin-left:13.05em;}.yui-t2 .yui-b{float:left;width:13.8461em;*width:13.50em;}.yui-t2 #yui-main .yui-b{margin-left:14.8461em;*margin-left:14.55em;}.yui-t3 .yui-b{float:left;width:23.0769em;*width:22.50em;}.yui-t3 #yui-main .yui-b{margin-left:24.0769em;*margin-left:23.62em;}.yui-t4 .yui-b{float:right;width:13.8456em;*width:13.50em;}.yui-t4 #yui-main .yui-b{margin-right:14.8456em;*margin-right:14.55em;}.yui-t5 .yui-b{float:right;width:18.4615em;*width:18.00em;}.yui-t5 #yui-main .yui-b{margin-right:19.4615em;*margin-right:19.125em;}.yui-t6 .yui-b{float:right;width:23.0769em;*width:22.50em;}.yui-t6 #yui-main .yui-b{margin-right:24.0769em;*margin-right:23.62em;}.yui-t7 #yui-main .yui-b{display:block;margin:0 0 1em 0;}#yui-main .yui-b{float:none;width:auto;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf,.yui-gc .yui-u,.yui-gd .yui-g,.yui-g .yui-gc .yui-u,.yui-ge .yui-u,.yui-ge .yui-g,.yui-gf .yui-g,.yui-gf .yui-u{float:right;}.yui-g div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first,.yui-ge div.first,.yui-gf div.first,.yui-g .yui-gc div.first,.yui-g .yui-ge div.first,.yui-gc div.first div.first{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf{width:49.1%;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{width:32%;margin-left:1.99%;}.yui-gb .yui-u{*margin-left:1.9%;*width:31.9%;}.yui-gc div.first,.yui-gd .yui-u{width:66%;}.yui-gd div.first{width:32%;}.yui-ge div.first,.yui-gf .yui-u{width:74.2%;}.yui-ge .yui-u,.yui-gf div.first{width:24%;}.yui-g .yui-gb div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first{margin-left:0;}.yui-g .yui-g .yui-u,.yui-gb .yui-g .yui-u,.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u,.yui-ge .yui-g .yui-u,.yui-gf .yui-g .yui-u{width:49%;*width:48.1%;*margin-left:0;}.yui-g .yui-g .yui-u{width:48.1%;}.yui-g .yui-gb div.first,.yui-gb .yui-gb div.first{*margin-right:0;*width:32%;_width:31.7%;}.yui-g .yui-gc div.first,.yui-gd .yui-g{width:66%;}.yui-gb .yui-g div.first{*margin-right:4%;_margin-right:1.3%;}.yui-gb .yui-gc div.first,.yui-gb .yui-gd div.first{*margin-right:0;}.yui-gb .yui-gb .yui-u,.yui-gb .yui-gc .yui-u{*margin-left:1.8%;_margin-left:4%;}.yui-g .yui-gb .yui-u{_margin-left:1.0%;}.yui-gb .yui-gd .yui-u{*width:66%;_width:61.2%;}.yui-gb .yui-gd div.first{*width:31%;_width:29.5%;}.yui-g .yui-gc .yui-u,.yui-gb .yui-gc .yui-u{width:32%;_float:right;margin-right:0;_margin-left:0;}.yui-gb .yui-gc div.first{width:66%;*float:left;*margin-left:0;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf .yui-u{margin:0;}.yui-gb .yui-gb .yui-u{_margin-left:.7%;}.yui-gb .yui-g div.first,.yui-gb .yui-gb div.first{*margin-left:0;}.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u{*width:48.1%;*margin-left:0;}.yui-gb .yui-gd div.first{width:32%;}.yui-g .yui-gd div.first{_width:29.9%;}.yui-ge .yui-g{width:24%;}.yui-gf .yui-g{width:74.2%;}.yui-gb .yui-ge div.yui-u,.yui-gb .yui-gf div.yui-u{float:right;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf div.first{float:left;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf div.first{*width:24%;_width:20%;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf .yui-u{*width:73.5%;_width:65.5%;}.yui-ge div.first .yui-gd .yui-u{width:65%;}.yui-ge div.first .yui-gd div.first{width:32%;}#hd:after,#bd:after,#ft:after,.yui-g:after,.yui-gb:after,.yui-gc:after,.yui-gd:after,.yui-ge:after,.yui-gf:after{content:".";display:block;height:0;clear:both;visibility:hidden;}#hd,#bd,#ft,.yui-g,.yui-gb,.yui-gc,.yui-gd,.yui-ge,.yui-gf{zoom:1;} 8 | 9 | body{margin:10px;}h1{font-size:138.5%;}h2{font-size:123.1%;}h3{font-size:108%;}h1,h2,h3{margin:1em 0;}h1,h2,h3,h4,h5,h6,strong,dt{font-weight:bold;}optgroup{font-weight:normal;}abbr,acronym{border-bottom:1px dotted #000;cursor:help;}em{font-style:italic;}del{text-decoration:line-through;}blockquote,ul,ol,dl{margin:1em;}ol,ul,dl{margin-left:2em;}ol li{list-style:decimal outside;}ul li{list-style:disc outside;}dl dd{margin-left:1em;}th,td{border:1px solid #000;padding:.5em;}th{font-weight:bold;text-align:center;}caption{margin-bottom:.5em;text-align:center;}sup{vertical-align:super;}sub{vertical-align:sub;}p,fieldset,table,pre{margin-bottom:1em;}button,input[type="checkbox"],input[type="radio"],input[type="reset"],input[type="submit"]{padding:1px;} 10 | 11 | 12 | --------------------------------------------------------------------------------