├── .github ├── FUNDING.yml └── workflows │ ├── build_documentation.yml │ └── run_tests.yml ├── .gitignore ├── tests ├── .gitignore ├── helping │ ├── ref │ │ └── 1.png │ └── test.typ ├── show-example │ ├── ref │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ └── test.typ ├── old-parser │ ├── test.typ │ ├── parse-argument-list.typ │ ├── currying.typ │ └── parse-module.typ ├── new-parser │ ├── test.typ │ ├── module-description.typ │ ├── description.typ │ ├── issue_#40.typ │ ├── currying.typ │ ├── multiple-definitions.typ │ ├── arguments.typ │ ├── argument-type.typ │ └── argument-description.typ ├── testing │ └── test.typ └── test_parse.typ ├── src ├── styles.typ ├── tidy.typ ├── testing.typ ├── utilities.typ ├── locales.typ ├── styles │ ├── help.typ │ ├── minimal.typ │ └── default.typ ├── parse-module.typ ├── show-module.typ ├── show-example.typ ├── helping.typ ├── new-parser.typ └── old-parser.typ ├── examples ├── wiggly-doc.typ ├── sincx.typ ├── repeater.typ ├── sincx-doc.typ ├── funny-math │ ├── funny-math-complex.typ │ ├── funny-math.typ │ ├── polar.svg │ ├── template.typ │ ├── funny-math-docs.typ │ └── custom-style.typ ├── wiggly.typ └── example-demo.typ ├── typst.toml ├── test.typ ├── LICENSE ├── docs ├── template.typ ├── migration-to-0.4.0.md └── tidy-guide.typ └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Mc-Zen] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pdf 3 | 4 | /*.txt 5 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | **/out/ 3 | **/diff/ 4 | -------------------------------------------------------------------------------- /tests/helping/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tidy/HEAD/tests/helping/ref/1.png -------------------------------------------------------------------------------- /tests/show-example/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tidy/HEAD/tests/show-example/ref/1.png -------------------------------------------------------------------------------- /tests/show-example/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tidy/HEAD/tests/show-example/ref/2.png -------------------------------------------------------------------------------- /tests/show-example/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tidy/HEAD/tests/show-example/ref/3.png -------------------------------------------------------------------------------- /tests/show-example/ref/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tidy/HEAD/tests/show-example/ref/4.png -------------------------------------------------------------------------------- /tests/show-example/ref/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tidy/HEAD/tests/show-example/ref/5.png -------------------------------------------------------------------------------- /tests/show-example/ref/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tidy/HEAD/tests/show-example/ref/6.png -------------------------------------------------------------------------------- /tests/show-example/ref/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tidy/HEAD/tests/show-example/ref/7.png -------------------------------------------------------------------------------- /tests/show-example/ref/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tidy/HEAD/tests/show-example/ref/8.png -------------------------------------------------------------------------------- /tests/show-example/ref/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mc-Zen/tidy/HEAD/tests/show-example/ref/9.png -------------------------------------------------------------------------------- /src/styles.typ: -------------------------------------------------------------------------------- 1 | #import "styles/default.typ" 2 | #import "styles/minimal.typ" 3 | #import "styles/help.typ" 4 | -------------------------------------------------------------------------------- /tests/old-parser/test.typ: -------------------------------------------------------------------------------- 1 | #include "parse-argument-list.typ" 2 | #include "parse-module.typ" 3 | #include "currying.typ" -------------------------------------------------------------------------------- /tests/new-parser/test.typ: -------------------------------------------------------------------------------- 1 | 2 | #include "multiple-definitions.typ" 3 | #include "description.typ" 4 | #include "arguments.typ" 5 | #include "argument-description.typ" 6 | #include "argument-type.typ" 7 | #include "currying.typ" 8 | #include "issue_#40.typ" 9 | 10 | -------------------------------------------------------------------------------- /examples/wiggly-doc.typ: -------------------------------------------------------------------------------- 1 | #import "/src/tidy.typ": * 2 | #import "wiggly.typ" 3 | 4 | #let docs = parse-module( 5 | read("/examples/wiggly.typ"), 6 | name: "wiggly", 7 | scope: (wiggly: wiggly), 8 | preamble: "#import wiggly: *\n" 9 | ) 10 | #show-module(docs, style: styles.minimal) 11 | -------------------------------------------------------------------------------- /examples/sincx.typ: -------------------------------------------------------------------------------- 1 | /// This function computes the cardinal sine, $sinc(x)=sin(x)/x$. 2 | /// 3 | /// ```example 4 | /// #sinc(0) 5 | /// ``` 6 | /// 7 | /// -> float 8 | #let sinc( 9 | /// The argument for the cardinal sine function. 10 | /// -> int | float 11 | x 12 | ) = if x == 0 {1} else {calc.sin(x) / x} -------------------------------------------------------------------------------- /typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tidy" 3 | version = "0.4.3" 4 | entrypoint = "src/tidy.typ" 5 | authors = ["Mc-Zen "] 6 | license = "MIT" 7 | description = "Documentation generator for Typst code in Typst." 8 | 9 | repository = "https://github.com/Mc-Zen/tidy" 10 | categories = ["utility", "scripting", "model"] 11 | compiler = "0.11.0" 12 | exclude = ["/docs/*"] -------------------------------------------------------------------------------- /.github/workflows/build_documentation.yml: -------------------------------------------------------------------------------- 1 | 2 | 3 | name: Build Documentation 4 | on: workflow_dispatch 5 | 6 | jobs: 7 | build_typst_documents: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Typst 13 | uses: lvignoli/typst-action@main 14 | with: 15 | source_file: docs/tidy-guide.typ 16 | -------------------------------------------------------------------------------- /examples/repeater.typ: -------------------------------------------------------------------------------- 1 | /// Repeats content a specified number of times. 2 | /// -> content 3 | #let repeat( 4 | /// The content to repeat. -> content 5 | body, 6 | 7 | /// Number of times to repeat the content. -> int 8 | num, 9 | 10 | /// Optional separator between repetitions of the content. -> content 11 | separator: [] 12 | ) = ((body,)*num).join(separator) 13 | 14 | /// An awfully bad approximation of pi. 15 | /// -> float 16 | #let awful-pi = 3.14 -------------------------------------------------------------------------------- /examples/sincx-doc.typ: -------------------------------------------------------------------------------- 1 | #import "/src/tidy.typ" 2 | #set text(font: "Arial") 3 | 4 | #set page(width: auto, height: auto, margin: 0em) 5 | 6 | 7 | 8 | #import "/examples/sincx.typ" 9 | 10 | #let docs = tidy.parse-module( 11 | read("/examples/sincx.typ"), 12 | scope: (sincx: sincx), 13 | preamble: "#import sincx: *;" 14 | ) 15 | 16 | #set heading(numbering: none) 17 | #block( 18 | width: 12cm, 19 | fill: luma(255), 20 | inset: 20pt, 21 | tidy.show-module(docs, show-outline: false) 22 | ) 23 | -------------------------------------------------------------------------------- /tests/new-parser/module-description.typ: -------------------------------------------------------------------------------- 1 | 2 | #import "/src/new-parser.typ": * 3 | 4 | 5 | #let src = ``` 6 | ///Module description 7 | 8 | let a() 9 | let func( 10 | ... 11 | ) = {} 12 | ```.text 13 | 14 | #assert.eq( 15 | parse(src).description, 16 | "Module description" 17 | ) 18 | 19 | 20 | #let src = ``` 21 | // License info 22 | 23 | // more stuff 24 | 25 | ///Module description 26 | ///... 27 | ```.text 28 | 29 | #assert.eq( 30 | parse(src).description, 31 | "Module description\n..." 32 | ) 33 | 34 | #assert.eq(parse(src).functions, ()) 35 | 36 | -------------------------------------------------------------------------------- /tests/new-parser/description.typ: -------------------------------------------------------------------------------- 1 | #import "/src/new-parser.typ": * 2 | 3 | // Variables 4 | 5 | #let src = ``` 6 | ///Description 7 | let var = 23 8 | ```.text 9 | 10 | #assert.eq( 11 | parse(src).variables, 12 | ( 13 | ( 14 | name: "var", 15 | description: "Description" 16 | ), 17 | ) 18 | ) 19 | 20 | 21 | // Functions 22 | 23 | #let src = ``` 24 | ///Description 25 | let func() = { 34 } 26 | ```.text 27 | 28 | #assert.eq( 29 | parse(src).functions, 30 | ( 31 | ( 32 | name: "func", 33 | description: "Description", 34 | args: (:), 35 | return-types: none 36 | ), 37 | ) 38 | ) -------------------------------------------------------------------------------- /tests/testing/test.typ: -------------------------------------------------------------------------------- 1 | 2 | 3 | #import "/src/tidy.typ": * 4 | 5 | 6 | 7 | #{ 8 | let code = ``` 9 | 10 | /// >>> 2 == 2 11 | /// >>> ("e", 2) == ("e", 2) 12 | /// >>> eq(1+1, 2) 13 | /// >>> ne(2+1, 2) 14 | /// >>> approx(calc.sin(calc.pi), 0) 15 | /// 16 | /// >>> 2 == 2 17 | /// >>> eq(2, 2) 18 | /// >>> eq((a: 13, b: 21) + (a: 3), (a: 3, b:21)) 19 | #let f() 20 | ``` 21 | 22 | let result = show-module(parse-module(code.text)) 23 | } 24 | 25 | 26 | // disable tests 27 | #{ 28 | let code = ``` 29 | /// >>> 2 == 3 30 | #let f() 31 | ``` 32 | let result = show-module(parse-module(code.text), enable-tests: false) 33 | } 34 | -------------------------------------------------------------------------------- /test.typ: -------------------------------------------------------------------------------- 1 | // #import "@preview/tidy:0.3.0" 2 | #import "src/tidy.typ" 3 | 4 | #show raw.where(block: true): highlight 5 | 6 | #let module = ```` 7 | /// #example(``` 8 | /// [a #raw("foo") b] 9 | /// ```) 10 | let x = none 11 | 12 | /// #example(`[a #raw("foo") b]`) 13 | let y = none 14 | 15 | /// #example(raw(`[a #raw("foo") b]`.text), mode: "markup") 16 | let z = none 17 | 18 | /// #example(raw(lang: "typc", `[a #raw("foo") b]`.text)) 19 | let ß = none 20 | ````.text 21 | 22 | #let module = tidy.parse-module( 23 | module, 24 | // preamble: "set raw(block: false);" 25 | ) 26 | #tidy.show-module( 27 | module, 28 | sort-functions: none, 29 | style: tidy.styles.minimal, 30 | ) -------------------------------------------------------------------------------- /src/tidy.typ: -------------------------------------------------------------------------------- 1 | // Source code for the typst-doc package 2 | 3 | 4 | #import "styles.typ" 5 | #import "old-parser.typ" as tidy-parse 6 | #import "utilities.typ" 7 | #import "testing.typ" 8 | #import "show-example.typ" as show-example: render-examples 9 | #import "parse-module.typ": parse-module 10 | #import "show-module.typ": show-module 11 | #import "helping.typ" as helping: generate-help 12 | 13 | 14 | #let help(..args) = { 15 | let namespace = ( 16 | ".": ( 17 | read.with("/src/parse-module.typ"), 18 | read.with("/src/show-module.typ"), 19 | read.with("/src/helping.typ"), 20 | ) 21 | ) 22 | generate-help(namespace: namespace, package-name: "tidy")(..args) 23 | } 24 | -------------------------------------------------------------------------------- /examples/funny-math/funny-math-complex.typ: -------------------------------------------------------------------------------- 1 | 2 | /// Construct a complex number of the form 3 | /// $ z= a + i b in CC. $ 4 | /// 5 | /// -> float 6 | #let complex( 7 | /// Real part of the complex number. -> float 8 | real, 9 | /// Imaginary part of the complex number. -> float 10 | imag 11 | ) = { 12 | (radius * calc.cos(phi), radius * calc.sin(phi)) 13 | } 14 | 15 | 16 | /// Construct a complex number from polar coordinates: @@funny-sqrt() 17 | /// $ z= r e^(i phi) in CC. $ 18 | /// #image-polar 19 | /// -> float 20 | #let polar( 21 | /// Angle to the real axis. -> float 22 | phi, 23 | /// Radius (euclidian distance to the origin). -> float 24 | radius: 1.0 25 | ) = { 26 | (radius * calc.cos(phi), radius * calc.sin(phi)) 27 | } -------------------------------------------------------------------------------- /examples/funny-math/funny-math.typ: -------------------------------------------------------------------------------- 1 | #import "funny-math-complex.typ": * 2 | 3 | 4 | /// This function computes the sine $sin(x)$ of $x$. 5 | /// 6 | /// See also @@funny-sqrt() for computing square roots. 7 | /// 8 | /// -> float 9 | #let funny-sin( 10 | /// Angle for the sine function. -> float 11 | phi 12 | ) = { calc.sqrt(phi) } 13 | 14 | 15 | 16 | 17 | /// This function computes the square root $sqrt(x)$ of it's argument. 18 | /// 19 | /// 20 | /// === Example 21 | /// ```example 22 | /// #funny-math.funny-sqrt(12) 23 | /// ``` 24 | /// 25 | /// 26 | /// -> float 27 | #let funny-sqrt( 28 | /// Argument to take the square root of. For $x=0$, the result is $0$: 29 | /// ```example 30 | /// #funny-math.funny-sqrt(0) 31 | /// ``` 32 | /// -> float | x 33 | x 34 | ) = { calc.sqrt(x) } 35 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches: [ main ] 5 | paths: 6 | - src/** 7 | - tests/** 8 | - .github/** 9 | pull_request: 10 | branches: [ main ] 11 | 12 | 13 | jobs: 14 | tests: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Install tytanic (binary) 21 | uses: taiki-e/install-action@v2 22 | with: 23 | tool: tytanic@0.3.0 24 | 25 | - name: Run test suite 26 | run: tt run --no-fail-fast 27 | 28 | - name: Archive artifacts 29 | uses: actions/upload-artifact@v4 30 | if: always() 31 | with: 32 | name: artifacts 33 | path: | 34 | tests/**/diff/*.png 35 | tests/**/out/*.png 36 | tests/**/ref/*.png 37 | retention-days: 5 -------------------------------------------------------------------------------- /tests/new-parser/issue_#40.typ: -------------------------------------------------------------------------------- 1 | #import "/src/new-parser.typ": * 2 | 3 | 4 | // Default args as strings containing "//" 5 | 6 | #let src = ``` 7 | ///Description 8 | let func( 9 | link: "//" 10 | ) 11 | ```.text 12 | 13 | #assert.eq( 14 | parse(src).functions, 15 | ( 16 | ( 17 | name: "func", 18 | description: "Description", 19 | args: ( 20 | link: (default: "\"//\"", description: ""), 21 | ), 22 | return-types: none 23 | ), 24 | ) 25 | ) 26 | 27 | 28 | // Check that comments still work 29 | #let src = ``` 30 | ///Description 31 | let func( 32 | pos, // some comment 33 | ) 34 | ```.text 35 | 36 | 37 | #assert.eq( 38 | parse(src).functions, 39 | ( 40 | ( 41 | name: "func", 42 | description: "Description", 43 | args: ( 44 | pos: (description: ""), 45 | ), 46 | return-types: none 47 | ), 48 | ) 49 | ) 50 | 51 | -------------------------------------------------------------------------------- /examples/wiggly.typ: -------------------------------------------------------------------------------- 1 | /// Draw a sine function with $n$ periods into a rectangle of given size. 2 | /// 3 | /// *Example:* 4 | /// 5 | /// ```example 6 | /// #draw-sine(1cm, 0.5cm, 2) 7 | /// ``` 8 | /// 9 | /// -> content 10 | #let draw-sine( 11 | 12 | /// Height of bounding rectangle. -> length 13 | width, 14 | 15 | /// Width of bounding rectangle. -> length 16 | height, 17 | 18 | /// Number of periods to draw. 19 | /// 20 | /// Example with many periods: 21 | /// ```example 22 | /// #draw-sine(4cm, 1.3cm, 10) 23 | /// ``` 24 | /// -> int | float 25 | periods 26 | ) = box(width: width, height: height, { 27 | let resolution = 100 28 | let frequency = 1 / resolution * 2 * calc.pi * periods 29 | let prev-point = (0pt, height / 2) 30 | for i in range(1, resolution) { 31 | let x = i / resolution * width 32 | let y = (1 - calc.sin(i * frequency)) * height / 2 33 | place(line(start: prev-point, end: (x, y))) 34 | prev-point = (x, y) 35 | } 36 | }) -------------------------------------------------------------------------------- /examples/funny-math/polar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | ϑ 10 | 11 | -------------------------------------------------------------------------------- /tests/new-parser/currying.typ: -------------------------------------------------------------------------------- 1 | 2 | #import "/src/parse-module.typ": * 3 | 4 | #let src = ``` 5 | /// Doc 6 | #let aey = text.with(1pt, weight: 200) 7 | ```.text 8 | 9 | #assert.eq( 10 | parse-module(src).functions.at(0), 11 | ( 12 | name: "aey", 13 | description: "Doc", 14 | args: (:), 15 | parent: ( 16 | name: "text", 17 | pos: ("1pt",), 18 | named: (weight: "200") 19 | ), 20 | return-types: none 21 | ), 22 | ) 23 | 24 | 25 | // Parent resolving 26 | 27 | #let src = ``` 28 | /// -> any 29 | let my-text( 30 | /// The weight 31 | /// -> int 32 | weight: 400, 33 | body 34 | ) 35 | 36 | /// Doc 37 | #let aey = my-text.with(weight: 200) 38 | ```.text 39 | 40 | #assert.eq( 41 | parse-module(src).functions.at(1), 42 | ( 43 | name: "aey", 44 | description: "Doc", 45 | args: ( 46 | body: (description: ""), 47 | weight: (description: "The weight", types: ("int",), default: "200") 48 | ), 49 | parent: ( 50 | name: "my-text", 51 | pos: (), 52 | named: (weight: "200") 53 | ), 54 | return-types: ("any",) 55 | ), 56 | ) 57 | 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 Mc-Zen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/old-parser/parse-argument-list.typ: -------------------------------------------------------------------------------- 1 | #import "/src/old-parser.typ": * 2 | 3 | 4 | //// General test 5 | #{ 6 | let str = "#let func(p1, p2: 3pt, p3: (), p4: (entries: ()), p5, \"as:d\")" 7 | let (pos, named, sink, count) = parse-argument-list(str, 9) 8 | assert.eq(count, str.len() - 9) 9 | assert.eq(pos.len(), 3) 10 | assert.eq(named.len(), 3) 11 | assert.eq(pos.at(0), "p1") 12 | assert.eq(pos.at(1), "p5") 13 | assert.eq(pos.at(2), "\"as:d\"") 14 | assert.eq(named.p2, "3pt") 15 | assert.eq(named.p3, "()") 16 | assert.eq(named.p4, "(entries: ())") 17 | assert.eq(named.p2, "3pt") 18 | } 19 | 20 | 21 | // Comma after last argument 22 | #{ 23 | let str = "#let func(p1,)" 24 | let (pos, named, sink, count) = parse-argument-list(str, 9) 25 | assert.eq(count, str.len() - 9) 26 | assert.eq(pos.len(), 1) 27 | } 28 | 29 | 30 | // line breaks in argument list 31 | #{ 32 | let str = "#let func(p1\n,p2\n,\n\n)\n" 33 | let (pos, named, sink, count) = parse-argument-list(str, 9) 34 | assert.eq(count, str.len() - 10) 35 | assert.eq(pos.len(), 2) 36 | } 37 | 38 | // #parse-argument-list("#let func(p1, p2: 3pt, p3: (), p4: (entries: ()), p5)", 9) 39 | 40 | -------------------------------------------------------------------------------- /tests/old-parser/currying.typ: -------------------------------------------------------------------------------- 1 | 2 | #import "/src/parse-module.typ": * 3 | 4 | #let parse-module = parse-module.with(old-syntax: true) 5 | #let src = ``` 6 | /// Doc 7 | #let aey = text.with(1pt, weight: 200) 8 | ```.text 9 | 10 | #assert.eq( 11 | parse-module(src).functions, 12 | ( 13 | ( 14 | name: "aey", 15 | description: " Doc\n\n", 16 | args: (:), 17 | parent: ( 18 | name: "text", 19 | pos: ("1pt",), 20 | named: (weight: "200") 21 | ), 22 | ), 23 | ) 24 | ) 25 | 26 | 27 | // Parent resolving 28 | 29 | #let src = ``` 30 | /// - weight (int): The weight 31 | /// -> any 32 | let my-text( 33 | weight: 400, 34 | body 35 | ) 36 | 37 | /// Doc 38 | #let aey = my-text.with(weight: 200) 39 | ```.text 40 | 41 | #assert.eq( 42 | parse-module(src).functions.at(1), 43 | ( 44 | name: "aey", 45 | description: " Doc\n\n", 46 | parent: ( 47 | name: "my-text", 48 | pos: (), 49 | named: (weight: "200") 50 | ), 51 | args: ( 52 | body: (:), 53 | weight: (description: "The weight", types: ("int",), default: "200") 54 | ), 55 | return-types: ("any",) 56 | ), 57 | ) 58 | 59 | -------------------------------------------------------------------------------- /examples/funny-math/template.typ: -------------------------------------------------------------------------------- 1 | // The project function defines how your document looks. 2 | // It takes your content and some metadata and formats it. 3 | // Go ahead and customize it to your liking! 4 | #let project( 5 | title: "", 6 | subtitle: "", 7 | abstract: [], 8 | authors: (), 9 | date: none, 10 | body, 11 | ) = { 12 | // Set the document's basic properties. 13 | set document(author: authors, title: title) 14 | set page(numbering: "1", number-align: center) 15 | set text(font: "Linux Libertine", lang: "en") 16 | // show heading.where(level: 1): set heading(numbering: "1") 17 | 18 | v(2em) 19 | 20 | // Title row. 21 | align(center)[ 22 | #block(text(weight: 700, 1.75em, title)) 23 | #block(text(1.0em, subtitle)) 24 | #v(4em, weak: true) 25 | #date 26 | ] 27 | 28 | // Author information. 29 | pad( 30 | top: 0.5em, 31 | x: 2em, 32 | grid( 33 | columns: (1fr,) * calc.min(3, authors.len()), 34 | gutter: 1em, 35 | ..authors.map(author => align(center, strong(author))), 36 | ), 37 | ) 38 | 39 | // Abstract. 40 | pad( 41 | x: 2em, 42 | top: 1em, 43 | bottom: 1.1em, 44 | align(center)[ 45 | #heading( 46 | outlined: false, 47 | numbering: none, 48 | text(0.85em, smallcaps[Abstract]), 49 | ) 50 | #abstract 51 | ], 52 | ) 53 | 54 | // Main body. 55 | set par(justify: true) 56 | v(4em) 57 | 58 | body 59 | } -------------------------------------------------------------------------------- /tests/new-parser/multiple-definitions.typ: -------------------------------------------------------------------------------- 1 | #import "/src/new-parser.typ": * 2 | 3 | 4 | 5 | // Multiple functions 6 | 7 | #let src = ``` 8 | ///Description 9 | let func(named: 2) 10 | ///Description 11 | let func1(named: 2) 12 | ```.text 13 | 14 | #assert.eq( 15 | parse(src).functions, 16 | ( 17 | ( 18 | name: "func", 19 | description: "Description", 20 | args: (named:(description: "", default: "2"),), 21 | return-types: none 22 | ), 23 | ( 24 | name: "func1", 25 | description: "Description", 26 | args: (named: (description: "", default: "2"),), 27 | return-types: none 28 | ), 29 | ) 30 | ) 31 | 32 | 33 | /// Description is not connected to definition 34 | 35 | #let src = ``` 36 | ///Description 37 | ///... 38 | 39 | let func( 40 | ... 41 | ) = {} 42 | ```.text 43 | 44 | #assert.eq( 45 | parse(src).functions, 46 | ( 47 | 48 | ) 49 | ) 50 | 51 | 52 | // Undocumented second function 53 | 54 | #let src = ``` 55 | ///Description 56 | let a() 57 | let func( 58 | ... 59 | ) = {} 60 | ```.text 61 | 62 | #assert.eq( 63 | parse(src).functions, 64 | ( 65 | (name: "a", description: "Description", args: (:), return-types: none), 66 | ) 67 | ) 68 | 69 | 70 | 71 | 72 | #let src = ``` 73 | /// Doc 74 | #let aey(x) 75 | /// No Doc 76 | 77 | #let bey(x) 78 | ```.text 79 | 80 | #assert.eq( 81 | parse(src).functions, 82 | ( 83 | (name: "aey", description: "Doc", args: (x: (description: ""),), return-types: none), 84 | // (name: "bey", description: "No Doc", args: ((name: "x", description: ""),)), 85 | ) 86 | ) 87 | -------------------------------------------------------------------------------- /examples/example-demo.typ: -------------------------------------------------------------------------------- 1 | /// #set text(size: .9em) 2 | /// ```/// #example(`#example-demo.flashy[We like our code flashy]`)``` 3 | /// #example(`#example-demo.flashy[We like our code flashy]`) 4 | /// ```/// #example(`#example-demo.flashy[Large previews will be scaled automatically to fit]`)``` 5 | /// #example(`#example-demo.flashy[Large previews will be scaled automatically to fit]`) 6 | /// ```/// #example(`#example-demo.flashy[Change code to preview ratio]`, ratio: 2)``` 7 | /// #example(`#example-demo.flashy[Change code to preview ratio]`, ratio: 2) 8 | /// ```/// #example(`#example-demo.flashy(map: color.map.vlag)[Huge preview]`, scale-preview: 200%)``` 9 | /// #example(`#example-demo.flashy(map: color.map.vlag)[Huge preview]`, scale-preview: 200%) 10 | /// ```/// #example(`#flashy[Add to scope #i ...]`, scope: (flashy: example-demo.flashy, i: 2))``` 11 | /// #example(`#flashy[Add to scope #i ...]`, scope: (flashy: example-demo.flashy, i: 2)) 12 | /// 13 | /// \ 14 | /// 15 | /// ```/// #example(`Markup *mode*`, mode: "markup")``` 16 | /// #example(`Markup *mode*`, mode: "markup") 17 | /// ```/// #example(`e^(i phi) = -1`, mode: "math")``` 18 | /// #example(`e^(i phi) = -1`, mode: "math") 19 | /// 20 | /// \ 21 | /// 22 | /// ```/// #example(`#example-demo.flashy(map: color.map.crest)[Very extremely long examples might maybe require the need of vertical layouting]`, dir: ttb)``` 23 | /// #example(`#example-demo.flashy(map: color.map.crest)[Very extremely long examples might maybe require the need of vertical layouting]`, dir: ttb) 24 | /// 25 | /// -> content 26 | #let flashy( 27 | /// -> content 28 | body, 29 | /// -> array 30 | map: color.map.spectral 31 | ) = highlight( 32 | body, fill: gradient.linear(..map) 33 | ) -------------------------------------------------------------------------------- /examples/funny-math/funny-math-docs.typ: -------------------------------------------------------------------------------- 1 | #import "template.typ": * 2 | // #import "@preview/tidy:0.1.0" 3 | #import "../../src/tidy.typ" 4 | #import "custom-style.typ" 5 | 6 | #show: project.with( 7 | title: "funny-math", 8 | subtitle: "Because math should be fun", 9 | authors: ("Euklid",), 10 | abstract: [*funny-math* is a funny math package for #link("https://typst.app/", [Typst]). ], 11 | date: "361 B.C.", 12 | ) 13 | 14 | // We can apply global styling here to affect the looks 15 | // of the documentation. 16 | #set text(font: "Calibri", size: 9pt) 17 | // #show heading: set text(size: 11pt) 18 | 19 | // Module name 20 | #show heading.where(level: 1): set text(size: 1.3em, font: "Cascadia Mono") 21 | 22 | // Function name 23 | #show heading.where(level: 2): set text(size: 1.2em, font: "Cascadia Mono") 24 | #show heading.where(level: 2): block.with(above: 3em, below: 2em) 25 | 26 | // "Parameters", "Example" 27 | #show heading.where(level: 3): set text(size: 1.1em, weight: "semibold") 28 | // #show heading.where(level: 3): block.with(spacing: 2em) 29 | #show heading.where(level: 4): set text(size: 1.1em, font: "Cascadia Mono") 30 | 31 | 32 | #pagebreak() 33 | 34 | #{ 35 | import "funny-math.typ" 36 | import "funny-math-complex.typ" 37 | let image-polar = image("polar.svg", width: 150pt) 38 | 39 | let show-module = tidy.show-module.with( 40 | first-heading-level: 1, 41 | style: custom-style 42 | ) 43 | 44 | let funny-module = tidy.parse-module( 45 | read("funny-math.typ"), 46 | name: "funny-math", 47 | label-prefix: "funny-math", 48 | scope: (funny-math: funny-math) 49 | ) 50 | show-module(funny-module) 51 | 52 | let funny-module-ext = tidy.parse-module( 53 | read("funny-math-complex.typ"), 54 | name: "funny-math.complex", 55 | label-prefix: "funny-math", 56 | scope: (funny-math-comples: funny-math-complex, image-polar: image-polar) 57 | ) 58 | 59 | pagebreak() 60 | // Also show the "complex" sub-module which belongs to the main module (funny-math.typ) since it is imported by it. 61 | show-module(funny-module-ext) 62 | } 63 | -------------------------------------------------------------------------------- /tests/show-example/test.typ: -------------------------------------------------------------------------------- 1 | #set page(width: 170pt, height: auto, margin: 0pt) 2 | #import "/src/tidy.typ": show-example as example, render-examples 3 | #let show-example = example.show-example.with( 4 | layout: (code, preview, ..sink) => { 5 | grid(columns: (1fr, 1fr), align: horizon, code, preview) 6 | } 7 | ) 8 | #let almost-default-show-example = example.show-example.with( 9 | layout: example.default-layout-example.with( 10 | code-block: block.with(stroke: .5pt + luma(200)), 11 | col-spacing: 0pt 12 | ) 13 | ) 14 | 15 | #set block(below: 0pt) 16 | 17 | // All possible combinations of code and markup mode 18 | #show-example(`1`) 19 | #show-example(`#calc.sin(0)`) 20 | #show-example(raw("#calc.sin(0)")) 21 | #show-example(raw(lang: "typc", "calc.sin(0)")) 22 | #show-example(raw(lang: "typc", "calc.sin(0)", block: true)) 23 | #show-example(raw(lang: "typ", "#calc.sin(0)")) 24 | #show-example(raw(lang: "typ", "a^2"), mode: "math") 25 | #show-example(raw(lang: "typm", "a^2"), mode: "math") 26 | 27 | #pagebreak() 28 | 29 | // Check that `raw` is not forced to block in the preview, see #21 30 | #show-example(`a #raw("foo") b`, mode: "markup") 31 | // Language should NOT default to typ in the preview, see #21 32 | #show-example(`#raw("#import")`, mode: "markup") 33 | 34 | #pagebreak() 35 | 36 | #almost-default-show-example(`Fit that code in a tiny space`) 37 | 38 | #pagebreak() 39 | 40 | // Test ratio 41 | #almost-default-show-example(`Ratio .5`, ratio: .5) 42 | #pagebreak() 43 | 44 | // Test scale-preview 45 | #almost-default-show-example(`Fit that 200%`, scale-preview: 200%) 46 | #almost-default-show-example(`Fit that 50%`, scale-preview: 50%) 47 | #pagebreak() 48 | 49 | 50 | // Test direction 51 | #almost-default-show-example(`#ltr`, dir: ltr) 52 | #pagebreak() 53 | #almost-default-show-example(`#ttb`, dir: ttb) 54 | 55 | #pagebreak() 56 | 57 | // The auto-shower, see #15 58 | 59 | #[ 60 | #show: render-examples 61 | 62 | ```example 63 | #rect(height: 3pt) 64 | ``` 65 | ] 66 | 67 | #pagebreak() 68 | 69 | 70 | #[ 71 | #set page(width: auto, height: auto, margin: 0pt) 72 | #show: render-examples.with(layout: (code, preview) => grid(columns: 2, align: horizon, code, preview)) 73 | 74 | ```example 75 | #rect(height: 3pt) 76 | ``` 77 | ] 78 | -------------------------------------------------------------------------------- /tests/helping/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/tidy.typ" 2 | 3 | #set page(width: 10cm, height: auto, margin: 2pt) 4 | 5 | #let heymath = ``` 6 | #import "vector.typ": * 7 | 8 | /// 9 | #let cos(x) = {} 10 | /// 11 | #let pi-squared = 12 | ```.text 13 | 14 | #let vector = ``` 15 | /// 16 | #let vec-add() 17 | /// 18 | #let vec-subtract() 19 | ```.text 20 | 21 | 22 | #let matrix = ``` 23 | #import "solve.typ" 24 | /// 25 | #let id( 26 | /// dimension -> int 27 | n 28 | ) 29 | ```.text 30 | 31 | #let solve = ``` 32 | /// 33 | #let solve(n) 34 | ```.text 35 | 36 | #let error = highlight 37 | 38 | 39 | #let help(..args) = { 40 | let namespace = ( 41 | ".": (() => heymath, () => vector), 42 | "matrix": () => matrix, 43 | "matrix.solve": () => solve 44 | ) 45 | tidy.generate-help( 46 | namespace: namespace, 47 | package-name: "heymath", 48 | onerror: msg => error(msg) 49 | )(..args) 50 | } 51 | 52 | #help("pi-squared") 53 | 54 | #let assert-not-failed(help-result) = { 55 | let body = help-result.body.children.at(1).child 56 | assert( 57 | body.func() != highlight, 58 | ) 59 | } 60 | 61 | // Valid calls 62 | #assert-not-failed(help("cos")) 63 | #assert-not-failed(help("pi-squared")) 64 | #assert-not-failed(help("vec-add")) 65 | #assert-not-failed(help("vec-subtract")) 66 | #assert-not-failed(help("matrix.id")) 67 | #assert-not-failed(help("matrix.solve.solve")) 68 | #assert-not-failed(help("matrix.id(n)")) 69 | #help("matrix.id(n)") 70 | 71 | // Invalid definition 72 | #assert.eq( 73 | help("ma"), 74 | tidy.helping.help-box( 75 | error("The package `heymath` contains no (documented) definition `ma`") 76 | ) 77 | ) 78 | 79 | 80 | // Invalid submodule 81 | #assert.eq( 82 | help("matrixs.id"), 83 | tidy.helping.help-box( 84 | error("The package `heymath` contains no module `matrixs`") 85 | ) 86 | ) 87 | 88 | 89 | 90 | // Invalid submodule 91 | #assert.eq( 92 | help("matrix.id(aaaa)"), 93 | tidy.helping.help-box( 94 | error("The function `matrix.id` has no parameter `aaaa`") 95 | ) 96 | ) 97 | 98 | 99 | // Invalid submodule 100 | #assert.eq( 101 | help("cos(aaaa)"), 102 | tidy.helping.help-box( 103 | error("The function `cos` has no parameter `aaaa`") 104 | ) 105 | ) 106 | 107 | 108 | // #help("vec-add") 109 | // #help("vec-subtract") 110 | // #help("matrixs.id") 111 | // #help("matrix.asd") 112 | // #help("matrix.solve.solve") 113 | // #help("cos(x)") 114 | // #help("matrix.id") 115 | #help(search: "vec") -------------------------------------------------------------------------------- /tests/new-parser/arguments.typ: -------------------------------------------------------------------------------- 1 | #import "/src/new-parser.typ": * 2 | 3 | 4 | /// Positional arguments 5 | 6 | #let src = ``` 7 | ///Description 8 | let func(pos) 9 | ```.text 10 | 11 | #assert.eq( 12 | parse(src).functions, 13 | ( 14 | ( 15 | name: "func", 16 | description: "Description", 17 | args: (pos: (description: ""),), 18 | return-types: none 19 | ), 20 | ) 21 | ) 22 | 23 | 24 | /// Identifier not directly followed argument list 25 | 26 | #let src = ``` 27 | ///Description 28 | let func (pos) 29 | ```.text 30 | 31 | #assert.eq( 32 | parse(src).functions, 33 | ( 34 | ( 35 | name: "func", 36 | description: "Description", 37 | args: (pos: (description: ""),), 38 | return-types: none 39 | ), 40 | ) 41 | ) 42 | 43 | 44 | // Named arguments 45 | 46 | #let src = ``` 47 | ///Description 48 | let func(named: 2) 49 | ```.text 50 | 51 | #assert.eq( 52 | parse(src).functions, 53 | ( 54 | ( 55 | name: "func", 56 | description: "Description", 57 | args: (named: (description: "", default: "2"),), 58 | return-types: none 59 | ), 60 | ) 61 | ) 62 | 63 | 64 | // Complex default for named argument. 65 | 66 | #let src = ``` 67 | ///Description 68 | ///... 69 | let func( 70 | named: (a: 12, b: (1+1)) 71 | ) = {} 72 | ```.text 73 | 74 | #assert.eq( 75 | parse(src).functions, 76 | ( 77 | ( 78 | name: "func", 79 | description: "Description\n...", 80 | args: ( 81 | named: (description: "", default: "(a: 12, b: (1+1))"), 82 | ), 83 | return-types: none 84 | ), 85 | ) 86 | ) 87 | 88 | 89 | // Multiline default arguments, see https://github.com/Mc-Zen/tidy/issues/41 90 | 91 | #let src = ````` 92 | /// -> float 93 | #let func( 94 | code: ```py 95 | i = 1 96 | while i < 10: 97 | print(i) 98 | i += 1 99 | ```, 100 | dict: ( 101 | a: 1, 102 | b: 2, 103 | c: 3, 104 | ) 105 | ) = { 106 | return 0 107 | } 108 | `````.text 109 | 110 | #assert.eq( 111 | parse(src).functions, 112 | ( 113 | ( 114 | name: "func", 115 | description: "", 116 | args: ( 117 | code: ( 118 | description: "", 119 | default: "```py\ni = 1\nwhile i < 10:\n print(i)\n i += 1\n ```" 120 | ), 121 | dict: ( 122 | description: "", 123 | default: "(\n a: 1,\n b: 2,\n c: 3,\n )" 124 | ), 125 | ), 126 | return-types: ("float",) 127 | ), 128 | ) 129 | ) -------------------------------------------------------------------------------- /tests/new-parser/argument-type.typ: -------------------------------------------------------------------------------- 1 | #import "/src/new-parser.typ": * 2 | 3 | 4 | #let src = ``` 5 | ///Description 6 | ///... 7 | let func( 8 | ///param pos 9 | /// -> int | none 10 | pos, 11 | /// -> any 12 | named: 2,) 13 | ```.text 14 | 15 | #assert.eq( 16 | parse(src).functions, 17 | ( 18 | ( 19 | name: "func", 20 | description: "Description\n...", 21 | args: ( 22 | pos: (description: "param pos", types: ("int", "none")), 23 | named: (description: "", default: "2", types: ("any",)), 24 | ), 25 | return-types: none 26 | ), 27 | ) 28 | ) 29 | 30 | 31 | #let src = ``` 32 | ///Description 33 | ///... 34 | let func( 35 | ///param pos -> int | array 36 | pos, 37 | ///param named -> any 38 | named: 2,) 39 | ```.text 40 | 41 | #assert.eq( 42 | parse(src).functions, 43 | ( 44 | ( 45 | name: "func", 46 | description: "Description\n...", 47 | args: ( 48 | pos: (description: "param pos", types: ("int", "array")), 49 | named: (description: "param named", default: "2", types: ("any",)), 50 | ), 51 | return-types: none 52 | ), 53 | ) 54 | ) 55 | 56 | 57 | 58 | 59 | // No argument type 60 | #let src = ``` 61 | /// 62 | #let edge( 63 | /// This is no problem -> int 64 | /// yes 65 | data, 66 | ) = {..} 67 | ```.text 68 | 69 | #assert.eq( 70 | parse(src).functions, 71 | ( 72 | ( 73 | name: "edge", 74 | description: "", 75 | args: ( 76 | data: (description: "This is no problem -> int\n yes"), 77 | ), 78 | return-types: none 79 | ), 80 | ) 81 | ) 82 | 83 | 84 | 85 | // Trailing argument type 86 | #let src = ```` 87 | /// 88 | #let edge( 89 | /// This is the problem -> int 90 | data, 91 | ) = {..} 92 | ````.text 93 | 94 | #assert.eq( 95 | parse(src).functions, 96 | ( 97 | ( 98 | name: "edge", 99 | description: "", 100 | args: ( 101 | data: (description: "This is the problem", types: ("int",)), 102 | ), 103 | return-types: none 104 | ), 105 | ) 106 | ) 107 | 108 | 109 | 110 | // Multiline argument type 111 | #let src = ``` 112 | /// 113 | #let edge( 114 | /// -> int | bool | string | 115 | /// array 116 | data, 117 | ) = {..} 118 | ```.text 119 | 120 | #assert.eq( 121 | parse(src).functions, 122 | ( 123 | ( 124 | name: "edge", 125 | description: "", 126 | args: ( 127 | data: (description: "", types: ("int", "bool", "string", "array")), 128 | ), 129 | return-types: none 130 | ), 131 | ) 132 | ) 133 | 134 | 135 | -------------------------------------------------------------------------------- /src/testing.typ: -------------------------------------------------------------------------------- 1 | 2 | /// Check for equality. 3 | #let eq(a, b) = { 4 | if a == b { return (true,) } 5 | else { 6 | return (false, repr(a) + " != " + repr(b)) 7 | } 8 | } 9 | 10 | /// Check for inequality. 11 | #let ne(a, b) = { 12 | if a != b { return (true,) } 13 | else { 14 | return (false, repr(a) + " == " + repr(b)) 15 | } 16 | } 17 | 18 | /// Check for approximate equality. 19 | #let approx(a, b, eps: 1e-10) = { 20 | if calc.abs(a - b) < eps { return (true,) } 21 | else { 22 | return (false, str(a) + " !≈ " + str(b)) 23 | } 24 | } 25 | 26 | #let assertations = ( 27 | eq: eq, 28 | ne: ne, 29 | approx: approx 30 | ) 31 | 32 | 33 | #let get-source-info-str(source-location) = { 34 | if source-location == none { return none } 35 | return "(" + source-location.module + ":" + str(source-location.line) + ")" 36 | } 37 | 38 | 39 | 40 | /// Implementation for doc-comment tests. All tests are run immediately. Fails if 41 | /// at least one test did not succeed. 42 | /// 43 | /// This function is made available in all doc-comments under the name 'test'. 44 | #let test( 45 | 46 | /// Tests to run in form of raw objects. 47 | /// -> any 48 | ..tests, 49 | 50 | /// Additional definitions to make available for the evaluated test code. 51 | /// -> dictionary 52 | scope: (:), 53 | 54 | /// Definitions that are made available to the entire parsed module including 55 | /// the test functions. This parameter is only used internally. 56 | /// -> dictionary 57 | inherited-scope: (:), 58 | 59 | /// Information about the location of the test source code. Should contain 60 | /// values for the keys `module` and `line`. This parameter is only used internally. 61 | /// -> dictionary 62 | source-location: none, 63 | 64 | /// When set to `false`, the tests are ignored. 65 | /// -> bool 66 | enable: true 67 | 68 | ) = { 69 | if not enable { return } 70 | let source-info = get-source-info-str(source-location) 71 | 72 | for test in tests.pos() { 73 | let result = eval(test.text, scope: scope + inherited-scope) 74 | let result-type = type(result) 75 | 76 | if result-type == array { 77 | if not result.at(0) { 78 | assert( 79 | false, 80 | message: "Failed test " + source-info + ": " 81 | + result.at(1) + "\nin " + test.text 82 | ) 83 | } 84 | } else if result-type == bool { 85 | if not result { 86 | let msg = test.text 87 | assert(false, message: "Failed test " + source-info + ": " + msg) 88 | } 89 | } else { 90 | assert( 91 | false, 92 | message: "Test \"" + test.text 93 | + "\" at " + source-info 94 | + " did not result in a boolean expression" 95 | ) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/new-parser/argument-description.typ: -------------------------------------------------------------------------------- 1 | #import "/src/new-parser.typ": * 2 | 3 | 4 | // Argument descriptions 5 | 6 | #let src = ``` 7 | ///Description 8 | let func( 9 | pos, // some comment 10 | named: 2 // another comment 11 | ) 12 | ```.text 13 | 14 | #assert.eq( 15 | parse(src).functions, 16 | ( 17 | ( 18 | name: "func", 19 | description: "Description", 20 | args: ( 21 | pos: (description: ""), 22 | named: (description: "", default: "2"), 23 | ), 24 | return-types: none 25 | ), 26 | ) 27 | ) 28 | 29 | // Just positional description 30 | 31 | #let src = ``` 32 | ///Description 33 | let func( 34 | ///param pos 35 | pos, 36 | named: 2 37 | ) 38 | ```.text 39 | 40 | #assert.eq( 41 | parse(src).functions, 42 | ( 43 | ( 44 | name: "func", 45 | description: "Description", 46 | args: ( 47 | pos: (description: "param pos"), 48 | named: (description: "", default: "2"), 49 | ), 50 | return-types: none 51 | ), 52 | ) 53 | ) 54 | 55 | // Just named description 56 | 57 | 58 | #let src = ``` 59 | ///Description 60 | let func( 61 | pos, 62 | ///param named 63 | named: 2 64 | ) 65 | ```.text 66 | 67 | #assert.eq( 68 | parse(src).functions, 69 | ( 70 | ( 71 | name: "func", 72 | description: "Description", 73 | args: ( 74 | pos: (description: ""), 75 | named: (description: "param named", default: "2"), 76 | ), 77 | return-types: none 78 | ), 79 | ) 80 | ) 81 | 82 | 83 | 84 | // Multiline argument descriptions 85 | 86 | #let src = ``` 87 | ///Description 88 | ///... 89 | let func( 90 | ///param pos 91 | ///... 92 | pos, 93 | ///param named 94 | ///... 95 | named: 2,) 96 | ```.text 97 | 98 | #assert.eq( 99 | parse(src).functions, 100 | ( 101 | ( 102 | name: "func", 103 | description: "Description\n...", 104 | args: ( 105 | pos: (description: "param pos\n..."), 106 | named: (description: "param named\n...", default: "2"), 107 | ), 108 | return-types: none 109 | ), 110 | ) 111 | ) 112 | 113 | 114 | // Argument descriptions with blank lines 115 | 116 | #let src = ``` 117 | ///Description 118 | let func( 119 | 120 | ///param pos 121 | ///... 122 | pos, 123 | 124 | 125 | ///param named 126 | ///... 127 | named: 2 128 | ) 129 | ```.text 130 | 131 | #assert.eq( 132 | parse(src).functions, 133 | ( 134 | ( 135 | name: "func", 136 | description: "Description", 137 | args: ( 138 | pos: (description: "param pos\n..."), 139 | named: (description: "param named\n...", default: "2"), 140 | ), 141 | return-types: none 142 | ), 143 | ) 144 | ) 145 | -------------------------------------------------------------------------------- /src/utilities.typ: -------------------------------------------------------------------------------- 1 | #import "locales.typ" 2 | 3 | // Matches doc-comment references of the form `@@otherfunc` or `@@otherfunc()`. 4 | #let reference-matcher = regex(`@@([\w\d\-_\)\(]+)`.text) 5 | 6 | 7 | /// Take a documentation string (for example a function or parameter 8 | /// description) and process doc-comment cross-references (starting with `@@`), 9 | /// turning them into links. 10 | #let process-references( 11 | 12 | /// Source code. -> str 13 | text, 14 | 15 | /// -> dictionary 16 | info 17 | 18 | ) = { 19 | return text.replace(reference-matcher, match => { 20 | let target = match.captures.at(0) 21 | if info.enable-cross-references { 22 | return "#(tidy.show-reference)(label(\"" + info.label-prefix + target + "\"), \"" + target + "\")" 23 | } else { 24 | return target 25 | } 26 | }) 27 | } 28 | 29 | 30 | 31 | /// Evaluate a doc-comment description (i.e., a function or parameter description) 32 | /// while processing cross-references (@@...) and providing the scope to the 33 | /// evaluation context. 34 | #let eval-docstring( 35 | 36 | /// Doc-comment to evaluate. -> str 37 | docstring, 38 | 39 | /// Object holding information for cross-reference processing and evaluation scope. 40 | /// -> dictionary 41 | info 42 | 43 | ) = { 44 | let scope = info.scope 45 | let content = process-references(docstring.trim(), info) 46 | eval(content, mode: "markup", scope: scope) 47 | } 48 | 49 | 50 | #let get-style-functions(style) = { 51 | // Default implementations for some style functions 52 | let show-reference(label, name, style-args) = link(label, raw(name)) 53 | 54 | import "styles.typ" 55 | let show-example = styles.default.show-example 56 | let show-variable = styles.default.show-variable 57 | 58 | let style-functions = style 59 | if type(style) == module { 60 | import style: * 61 | style-functions = ( 62 | show-outline: show-outline, 63 | show-type: show-type, 64 | show-function: show-function, 65 | show-parameter-list: show-parameter-list, 66 | show-parameter-block: show-parameter-block, 67 | show-reference: show-reference, 68 | show-example: show-example, 69 | show-variable: show-variable, 70 | ) 71 | } 72 | return style-functions 73 | } 74 | 75 | 76 | /// Get the local name for a string with the given language. 77 | #let get-local-name( 78 | 79 | /// String to get the local name for -> str 80 | target, 81 | 82 | /// Style-args provided from styles -> dict 83 | style-args: (:) 84 | 85 | ) = context { 86 | 87 | if target in style-args.local-names { 88 | return style-args.local-names.at(target) 89 | } 90 | let language = text.lang 91 | if language not in locales.local-names.keys() { 92 | panic("Unknown language '" + language + "', you can use custom translations with `#show-module(local-names: ...)`") 93 | } 94 | return locales.local-names.at(text.lang).at(target) 95 | } 96 | -------------------------------------------------------------------------------- /docs/template.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/codly:1.3.0": codly-init, no-codly, codly 2 | 3 | // The project function defines how your document looks. 4 | // It takes your content and some metadata and formats it. 5 | // Go ahead and customize it to your liking! 6 | #let project( 7 | title: "", 8 | subtitle: "", 9 | abstract: [], 10 | authors: (), 11 | url: none, 12 | date: none, 13 | version: none, 14 | body, 15 | ) = { 16 | // Set the document's basic properties. 17 | set document(author: authors, title: title) 18 | set page(numbering: "1", number-align: center) 19 | 20 | show heading.where(level: 1): it => block(smallcaps(it), below: 1em) 21 | // set heading(numbering: (..args) => if args.pos().len() == 1 { numbering("I", ..args) }) 22 | set heading(numbering: "I.a") 23 | show list: pad.with(x: 5%) 24 | show heading.where(level: 3): set text(1.2em) 25 | 26 | // show link: set text(fill: purple.darken(30%)) 27 | show link: it => { 28 | let dest = str(it.dest) 29 | if "." in dest and not "/" in dest { return underline(it, stroke: luma(60%), offset: 1pt) } 30 | set text(fill: rgb("#1e8f6f")) 31 | underline(it) 32 | } 33 | 34 | v(4em) 35 | 36 | // Title row. 37 | align(center)[ 38 | #block(text(weight: 700, 1.75em, title)) 39 | #block(text(1.0em, subtitle)) 40 | #v(4em, weak: true) 41 | v#version #h(1.2cm) #date 42 | #block(link(url)) 43 | #v(1.5em, weak: true) 44 | ] 45 | 46 | // Author information. 47 | pad( 48 | top: 0.5em, 49 | x: 2em, 50 | grid( 51 | columns: (1fr,) * calc.min(3, authors.len()), 52 | gutter: 1em, 53 | ..authors.map(author => align(center, strong(author))), 54 | ), 55 | ) 56 | 57 | v(3cm, weak: true) 58 | 59 | // Abstract. 60 | pad( 61 | x: 3.8em, 62 | top: 1em, 63 | bottom: 1.1em, 64 | align(center)[ 65 | #heading( 66 | outlined: false, 67 | numbering: none, 68 | text(0.85em, smallcaps[Abstract]), 69 | ) 70 | #abstract 71 | ], 72 | ) 73 | 74 | // Main body. 75 | set par(justify: true) 76 | v(7em) 77 | 78 | pad(x: 10%, outline(depth: 2, indent: 2em)) 79 | pagebreak() 80 | 81 | show: codly-init.with( 82 | ) 83 | codly( 84 | fill: white 85 | ) 86 | // codly( 87 | // languages: (: 88 | // // typ: (name: "typ", icon: none, color: rgb("#239DAE")), 89 | // ), 90 | // ) 91 | show raw.where(block: true): set text(size: .95em) 92 | show raw.where(block: true): it => pad(x: 4%, it) 93 | show raw.where(block: false, lang: "typ").or(raw.where(lang: "notnone")): it => box(inset: (x: 3pt), outset: (y: 3pt), radius: 40%, fill: luma(235), it) 94 | set raw(lang: "notnone") 95 | body 96 | } 97 | 98 | #let ref-fn(name) = link(label("tidy-" + name), raw(name)) 99 | 100 | #let file-code(filename, code) = pad(x: 4%, block( 101 | width: 100%, 102 | fill: rgb("#239DAE").lighten(80%), 103 | inset: 1pt, 104 | stroke: rgb("#239DAE") + 1pt, 105 | radius: 3pt, 106 | { 107 | block(align(right, text(raw(filename, lang: "cmd"))), width: 100%, inset: 5pt) 108 | v(1pt, weak: true) 109 | move(dx: -1pt, line(length: 100% + 2pt, stroke: 1pt + rgb("#239DAE"))) 110 | v(1pt, weak: true) 111 | pad(x: -4.3%, code) 112 | } 113 | )) 114 | 115 | 116 | #let tidy-output-figure( 117 | output, 118 | breakable: false, 119 | fill: none 120 | ) = no-codly({ 121 | set heading(numbering: none) 122 | set text(size: .8em) 123 | show figure: set block(breakable: breakable) 124 | figure(align(left, block( 125 | width: 80%, 126 | fill: fill, 127 | stroke: 0.5pt + luma(200), 128 | inset: 20pt, 129 | radius: 10pt, 130 | block( 131 | breakable: breakable, 132 | output 133 | ) 134 | ))) 135 | }) -------------------------------------------------------------------------------- /examples/funny-math/custom-style.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/tidy.typ" 2 | #import tidy.utilities: * 3 | 4 | // Color to highlight function names in 5 | #let fn-color = rgb("#4b69c6") 6 | 7 | 8 | // Colors for Typst types 9 | #let type-colors = ( 10 | "content": rgb("#a6ebe6"), 11 | "color": rgb("#a6ebe6"), 12 | "string": rgb("#d1ffe2"), 13 | "none": rgb("#ffcbc4"), 14 | "auto": rgb("#ffcbc4"), 15 | "bool": rgb("#ffedc1"), 16 | "boolean": rgb("#ffedc1"), 17 | "integer": rgb("#e7d9ff"), 18 | "float": rgb("#e7d9ff"), 19 | "ratio": rgb("#e7d9ff"), 20 | "length": rgb("#e7d9ff"), 21 | "angle": rgb("#e7d9ff"), 22 | "relative-length": rgb("#e7d9ff"), 23 | "fraction": rgb("#e7d9ff"), 24 | "symbol": rgb("#eff0f3"), 25 | "array": rgb("#eff0f3"), 26 | "dictionary": rgb("#eff0f3"), 27 | "arguments": rgb("#eff0f3"), 28 | "selector": rgb("#eff0f3"), 29 | "module": rgb("#eff0f3"), 30 | "stroke": rgb("#eff0f3"), 31 | "function": rgb("#f9dfff"), 32 | ) 33 | 34 | #let get-type-color(type) = type-colors.at(type, default: rgb("#eff0f3")) 35 | 36 | 37 | 38 | 39 | 40 | // Create beautiful, colored type box 41 | #let show-type(type) = { 42 | h(2pt) 43 | box(outset: 2pt, fill: get-type-color(type), radius: 2pt, raw(type)) 44 | h(2pt) 45 | } 46 | 47 | 48 | 49 | #let show-parameter-list(fn, display-type-function) = { 50 | pad(x: 10pt, { 51 | set text(font: "Cascadia Mono", size: 0.85em, weight: 340) 52 | text(fn.name, fill: fn-color) 53 | "(" 54 | let inline-args = fn.args.len() < 2 55 | if not inline-args { "\n " } 56 | let items = () 57 | for (arg-name, info) in fn.args { 58 | let types 59 | if "types" in info { 60 | types = ": " + info.types.map(x => display-type-function(x)).join(" ") 61 | } 62 | items.push(arg-name + types) 63 | } 64 | items.join( if inline-args {", "} else { ",\n "}) 65 | if not inline-args { "\n" } + ")" 66 | if fn.return-types != none { 67 | " -> " 68 | fn.return-types.map(x => display-type-function(x)).join(" ") 69 | } 70 | }) 71 | } 72 | 73 | 74 | // Create a parameter description block, containing name, type, description and optionally the default value. 75 | #let show-parameter-block( 76 | name, types, content, style-args, 77 | show-default: false, 78 | default: none, 79 | ) = block( 80 | inset: 10pt, fill: luma(98%), width: 100%, 81 | breakable: style-args.break-param-descriptions, 82 | [ 83 | #box(heading(level: style-args.first-heading-level + 3, name)) 84 | #h(.5cm) 85 | #types.map(x => (style-args.style.show-type)(x)).join([ #text("or",size:.6em) ]) 86 | 87 | #content 88 | #if show-default [ #parbreak() Default: #raw(lang: "typc", default) ] 89 | ] 90 | ) 91 | 92 | 93 | 94 | #let show-function( 95 | fn, style-args, 96 | ) = { 97 | [ 98 | #heading(fn.name, level: style-args.first-heading-level + 1) 99 | #label(style-args.label-prefix + fn.name + "()") 100 | ] 101 | 102 | eval-docstring(fn.description, style-args) 103 | 104 | block(breakable: style-args.break-param-descriptions, { 105 | heading("Parameters", level: style-args.first-heading-level + 2) 106 | (style-args.style.show-parameter-list)(fn, style-args.style.show-type) 107 | }) 108 | 109 | for (name, info) in fn.args { 110 | let types = info.at("types", default: ()) 111 | let description = info.at("description", default: "") 112 | if description == "" and style-args.omit-empty-param-descriptions { continue } 113 | (style-args.style.show-parameter-block)( 114 | name, types, eval-docstring(description, style-args), 115 | style-args, 116 | show-default: "default" in info, 117 | default: info.at("default", default: none), 118 | ) 119 | } 120 | v(4.8em, weak: true) 121 | } 122 | 123 | 124 | #let show-reference(label, name, style-args: none) = { 125 | let output 126 | if "no-namespace" in style-args { 127 | output = text(fill: fn-color, raw(name)) 128 | } else { 129 | output = text(fill: purple, raw("#" + style-args.label-prefix)) + text(fill: fn-color, raw("." + name)) 130 | } 131 | link(label, output) 132 | } 133 | 134 | 135 | #let show-outline(module-doc, style-args: none) = { 136 | let prefix = module-doc.label-prefix 137 | style-args.insert("no-namespace", none) 138 | let items = () 139 | for fn in module-doc.functions { 140 | // items.push(link(label(prefix + fn.name + "()"), fn.name + "()")) 141 | let ref = show-reference(label(prefix + fn.name + "()"), fn.name + "()", style-args: style-args) 142 | items.push(ref) 143 | } 144 | list(..items) 145 | } 146 | 147 | #import tidy.show-example: show-example as show-example-base 148 | 149 | #let show-example( 150 | ..args 151 | ) = { 152 | show-example-base( 153 | ..args, 154 | code-block: block.with(radius: 3pt, stroke: .5pt + luma(200)), 155 | preview-block: block.with(radius: 3pt, fill: rgb("#e4e5ea")), 156 | col-spacing: 5pt 157 | ) 158 | } -------------------------------------------------------------------------------- /src/locales.typ: -------------------------------------------------------------------------------- 1 | #let local-names = ( 2 | "sq": ( // Albanian 3 | parameters: [Parametrat], 4 | default: [Standard], 5 | variables: [Variablat] 6 | ), 7 | "ar": ( // Arabic 8 | parameters: [المعلمات], 9 | default: [الافتراضي], 10 | variables: [المتغيرات] 11 | ), 12 | "eu": ( // Basque 13 | parameters: [Parametroak], 14 | default: [Lehenetsia], 15 | variables: [Aldagaiak] 16 | ), 17 | "nb": ( // Bokmål 18 | parameters: [Parametere], 19 | default: [Standard], 20 | variables: [Variabler] 21 | ), 22 | "bg": ( // Bulgarian 23 | parameters: [Параметри], 24 | default: [По подразбиране], 25 | variables: [Променливи] 26 | ), 27 | "ca": ( // Catalan 28 | parameters: [Paràmetres], 29 | default: [Per defecte], 30 | variables: [Variables] 31 | ), 32 | "zh": ( // Chinese (Simplified) 33 | parameters: [参数], 34 | default: [默认值], 35 | variables: [变量] 36 | ), 37 | "hr": ( // Croatian 38 | parameters: [Parametri], 39 | default: [Zadano], 40 | variables: [Varijable] 41 | ), 42 | "cs": ( // Czech 43 | parameters: [Parametry], 44 | default: [Výchozí], 45 | variables: [Proměnné] 46 | ), 47 | "da": ( // Danish 48 | parameters: [Parametre], 49 | default: [Standard], 50 | variables: [Variabler] 51 | ), 52 | "nl": ( // Dutch 53 | parameters: [Parameters], 54 | default: [Standaard], 55 | variables: [Variabelen] 56 | ), 57 | "en": ( // English 58 | parameters: [Parameters], 59 | default: [Default], 60 | variables: [Variables] 61 | ), 62 | "et": ( // Estonian 63 | parameters: [Parameetrid], 64 | default: [Vaikimisi], 65 | variables: [Muutujad] 66 | ), 67 | "tl": ( // Filipino 68 | parameters: [Mga Parameter], 69 | default: [Karaniwan], 70 | variables: [Mga Variable] 71 | ), 72 | "fi": ( // Finnish 73 | parameters: [Parametrit], 74 | default: [Oletus], 75 | variables: [Muuttujat] 76 | ), 77 | "fr": ( // French 78 | parameters: [Paramètres], 79 | default: [Par défaut], 80 | variables: [Variables] 81 | ), 82 | "gl": ( // Galician 83 | parameters: [Parámetros], 84 | default: [Por defecto], 85 | variables: [Variables] 86 | ), 87 | "de": ( // German 88 | parameters: [Parameter], 89 | default: [Standard], 90 | variables: [Variablen] 91 | ), 92 | "el": ( // Greek 93 | parameters: [Παράμετροι], 94 | default: [Προεπιλογή], 95 | variables: [Μεταβλητές] 96 | ), 97 | "he": ( // Hebrew 98 | parameters: [פרמטרים], 99 | default: [ברירת מחדל], 100 | variables: [משתנים] 101 | ), 102 | "hu": ( // Hungarian 103 | parameters: [Paraméterek], 104 | default: [Alapértelmezett], 105 | variables: [Változók] 106 | ), 107 | "is": ( // Icelandic 108 | parameters: [Færibreytur], 109 | default: [Sjálfgefið], 110 | variables: [Breytur] 111 | ), 112 | "id": ( // Indonesian 113 | parameters: [Parameter], 114 | default: [Baku], 115 | variables: [Variabel] 116 | ), 117 | "it": ( // Italian 118 | parameters: [Parametri], 119 | default: [Predefinito], 120 | variables: [Variabili] 121 | ), 122 | "ja": ( // Japanese 123 | parameters: [パラメーター], 124 | default: [デフォルト], 125 | variables: [変数] 126 | ), 127 | "la": ( // Latin 128 | parameters: [Parametri], 129 | default: [Definitum], 130 | variables: [Variabilia] 131 | ), 132 | "dsb": ( // Lower Sorbian 133 | parameters: [Parametry], 134 | default: [Standard], 135 | variables: [Wariable] 136 | ), 137 | "nn": ( // Nynorsk 138 | parameters: [Parametrar], 139 | default: [Standard], 140 | variables: [Variablar] 141 | ), 142 | "pl": ( // Polish 143 | parameters: [Parametry], 144 | default: [Domyślne], 145 | variables: [Zmienne] 146 | ), 147 | "pt": ( // Portuguese 148 | parameters: [Parâmetros], 149 | default: [Padrão], 150 | variables: [Variáveis] 151 | ), 152 | "ro": ( // Romanian 153 | parameters: [Parametri], 154 | default: [Implicit], 155 | variables: [Variabile] 156 | ), 157 | "ru": ( // Russian 158 | parameters: [Параметры], 159 | default: [По умолчанию], 160 | variables: [Переменные] 161 | ), 162 | "sr": ( // Serbian (Cyrillic) 163 | parameters: [Параметри], 164 | default: [Подразумевано], 165 | variables: [Променљиве] 166 | ), 167 | "sk": ( // Slovak 168 | parameters: [Parametre], 169 | default: [Predvolené], 170 | variables: [Premenné] 171 | ), 172 | "sl": ( // Slovenian 173 | parameters: [Parametri], 174 | default: [Privzeto], 175 | variables: [Spremenljivke] 176 | ), 177 | "es": ( // Spanish 178 | parameters: [Parámetros], 179 | default: [Por defecto], 180 | variables: [Variables] 181 | ), 182 | "sv": ( // Swedish 183 | parameters: [Parametrar], 184 | default: [Standard], 185 | variables: [Variabler] 186 | ), 187 | "tr": ( // Turkish 188 | parameters: [Parametreler], 189 | default: [Varsayılan], 190 | variables: [Değişkenler] 191 | ), 192 | "uk": ( // Ukrainian 193 | parameters: [Параметри], 194 | default: [За замовчуванням], 195 | variables: [Змінні] 196 | ), 197 | "vi": ( // Vietnamese 198 | parameters: [Tham số], 199 | default: [Mặc định], 200 | variables: [Biến số] 201 | ), 202 | ) -------------------------------------------------------------------------------- /src/styles/help.typ: -------------------------------------------------------------------------------- 1 | #import "../utilities.typ": * 2 | #import "default.typ" 3 | 4 | 5 | // Color to highlight function names in 6 | #let fn-color = rgb("#1f2a63") 7 | #let fn-color = blue.darken(30%) 8 | 9 | #let default-type-color = rgb("#eff0f3") 10 | 11 | 12 | #let show-outline(module-doc, style-args: (:)) = { 13 | let prefix = module-doc.label-prefix 14 | let items = () 15 | for fn in module-doc.functions { 16 | items.push(fn.name + "()") 17 | // items.push(link(label(prefix + fn.name + "()"), fn.name + "()")) 18 | } 19 | list(..items) 20 | } 21 | 22 | #let show-type(type-name, style-args: (:)) = { 23 | h(2pt) 24 | let clr = style-args.colors.at(type-name, default: style-args.colors.at("default", default: default-type-color)) 25 | if type(clr) == color { 26 | let components = clr.components() 27 | clr = rgb(..components.slice(0, -1), 60%) 28 | } 29 | box(outset: 2pt, fill: clr, radius: 2pt, raw(type-name, lang: none)) 30 | h(2pt) 31 | } 32 | 33 | 34 | #let show-parameter-list(fn, style-args) = { 35 | block(fill: rgb("#d8dbed44"), width: 100%, inset: (x: 0.5em, y: 0.7em), { 36 | set text(font: "DejaVu Sans Mono", size: 0.85em, weight: 340) 37 | text(fn.name) 38 | "(" 39 | let inline-args = fn.args.len() < 5 40 | if not inline-args { "\n " } 41 | let items = () 42 | for (arg-name, info) in fn.args { 43 | let types 44 | if "types" in info { 45 | types = ": " + info.types.map(x => show-type(x, style-args: style-args)).join(" ") 46 | } 47 | items.push(box(arg-name + types)) 48 | } 49 | items.join( if inline-args {", "} else { ",\n "}) 50 | if not inline-args { "\n" } + ")" 51 | if fn.return-types != none { 52 | box[~-> #fn.return-types.map(x => show-type(x, style-args: style-args)).join(" ")] 53 | } 54 | }) 55 | } 56 | 57 | 58 | 59 | // Create a parameter description block, containing name, type, description and optionally the default value. 60 | #let show-parameter-block( 61 | name, types, content, style-args, 62 | show-default: false, 63 | default: none, 64 | ) = block( 65 | inset: 0pt, width: 100%, 66 | breakable: style-args.break-param-descriptions, 67 | [ 68 | #[ 69 | #raw(name, lang: none) 70 | ] 71 | #if types != () [ 72 | (#h(-.2em) 73 | #types.map(x => (style-args.style.show-type)(x, style-args: style-args)).join([ #text("or",size:.6em) ]) 74 | #if show-default [\= #raw(lang: "typc", default) ] 75 | #h(-.2em)) 76 | ] 77 | -- 78 | #content 79 | 80 | ] 81 | ) 82 | 83 | 84 | #let show-function( 85 | fn, style-args, 86 | ) = { 87 | if style-args.colors == auto { style-args.colors = default.colors } 88 | set par(justify: false, hanging-indent: 1em, first-line-indent: 0em) 89 | 90 | block(breakable: style-args.break-param-descriptions, fill: rgb("#d8dbed44"), 91 | if style-args.enable-cross-references [ 92 | #(style-args.style.show-parameter-list)(fn, style-args) 93 | #label(style-args.label-prefix + fn.name + "()") 94 | ] else [ 95 | #(style-args.style.show-parameter-list)(fn, style-args) 96 | ]) 97 | pad(x: 0em, eval-docstring(fn.description, style-args)) 98 | 99 | let parameter-block 100 | 101 | for (name, info) in fn.args { 102 | let types = info.at("types", default: ()) 103 | let description = info.at("description", default: "") 104 | if description == "" and style-args.omit-empty-param-descriptions { continue } 105 | parameter-block += (style-args.style.show-parameter-block)( 106 | name, types, eval-docstring(description, style-args), 107 | style-args, 108 | show-default: "default" in info, 109 | default: info.at("default", default: none), 110 | ) 111 | } 112 | 113 | if parameter-block != none { 114 | [*Parameters:*] 115 | parameter-block 116 | } 117 | v(2em, weak: true) 118 | } 119 | 120 | 121 | #let show-variable( 122 | var, style-args, 123 | ) = { 124 | if style-args.colors == auto { style-args.colors = default.colors } 125 | set par(justify: false, hanging-indent: 1em, first-line-indent: 0em) 126 | 127 | let type = if "type" not in var { none } 128 | else { show-type(var.type, style-args: style-args) } 129 | 130 | block(breakable: style-args.break-param-descriptions, fill: rgb("#d8dbed44"), width: 100%, inset: (x: 0.5em, y: 0.7em), 131 | stack(dir: ltr, spacing: 1.2em, 132 | if style-args.enable-cross-references [ 133 | #set text(font: "DejaVu Sans Mono", size: 0.85em, weight: 340) 134 | #text(var.name) 135 | #label(style-args.label-prefix + var.name) 136 | ] else [ 137 | #set text(font: "DejaVu Sans Mono", size: 0.85em, weight: 340) 138 | #text(var.name) 139 | ], 140 | type 141 | ) 142 | ) 143 | pad(x: 0em, eval-docstring(var.description, style-args)) 144 | 145 | v(2em, weak: true) 146 | } 147 | 148 | 149 | #let show-reference(label, name, style-args: none) = { 150 | link(label, raw(name, lang: none)) 151 | } 152 | 153 | #import "../show-example.typ" as example 154 | 155 | #let show-example( 156 | ..args 157 | ) = { 158 | 159 | example.show-example( 160 | ..args, 161 | layout: example.default-layout-example.with( 162 | code-block: block.with(radius: 3pt, stroke: .5pt + luma(200)), 163 | preview-block: block.with(radius: 3pt, fill: rgb("#e4e5ea")), 164 | col-spacing: 5pt 165 | ), 166 | ) 167 | } -------------------------------------------------------------------------------- /src/parse-module.typ: -------------------------------------------------------------------------------- 1 | #import "old-parser.typ" 2 | #import "new-parser.typ" 3 | #import "styles.typ" 4 | 5 | 6 | #let resolve-parents(function-docs) = { 7 | for i in range(function-docs.len()) { 8 | let docs = function-docs.at(i) 9 | if not "parent" in docs { continue } 10 | 11 | let parent = docs.at("parent", default: none) 12 | if parent == none { continue } 13 | 14 | let parent-docs = function-docs.find(x => x.name == parent.name) 15 | if parent-docs == none { continue } 16 | 17 | // Inherit args and return types from parent 18 | docs.args = parent-docs.args 19 | docs.return-types = parent-docs.return-types 20 | 21 | for (arg, value) in parent.named { 22 | assert(arg in docs.args) 23 | docs.args.at(arg).default = value 24 | } 25 | 26 | // Maybe strip some positional arguments 27 | if parent.pos.len() > 0 { 28 | let named-args = docs.args.pairs().filter(((_, info)) => "default" in info) 29 | let positional-args = docs.args.pairs().filter(((_, info)) => not "default" in info) 30 | assert(parent.pos.len() <= positional-args.len(), message: "Too many positional arguments") 31 | positional-args = positional-args.slice(parent.pos.len()) 32 | docs.args = (:) 33 | for (name, info) in positional-args + named-args { 34 | docs.args.insert(name, info) 35 | } 36 | } 37 | function-docs.at(i) = docs 38 | } 39 | return function-docs 40 | } 41 | 42 | 43 | #let old-parse( 44 | content, 45 | label-prefix: "", 46 | require-all-parameters: false, 47 | enable-curried-functions: true 48 | ) = { 49 | 50 | let parse-info = ( 51 | label-prefix: label-prefix, 52 | require-all-parameters: require-all-parameters, 53 | ) 54 | 55 | let module-docstring = old-parser.parse-module-docstring(content, parse-info) 56 | 57 | let matches = content.matches(old-parser.docstring-matcher) 58 | let function-docs = () 59 | let variable-docs = () 60 | 61 | for match in matches { 62 | 63 | if content.len() <= match.end or content.at(match.end) != "(" { 64 | let doc = old-parser.parse-variable-docstring(content, match, parse-info) 65 | if enable-curried-functions { 66 | let parent-info = old-parser.parse-curried-function(content, match.end) 67 | if parent-info == none { 68 | variable-docs.push(doc) 69 | } else { 70 | doc.parent = parent-info 71 | if "type" in doc { doc.remove("type") } 72 | doc.args = (:) 73 | function-docs.push(doc) 74 | } 75 | } else { 76 | variable-docs.push(doc) 77 | } 78 | } else { 79 | let function-doc = old-parser.parse-function-docstring(content, match, parse-info) 80 | function-docs.push(function-doc) 81 | } 82 | } 83 | return ( 84 | description: module-docstring, 85 | functions: function-docs, 86 | variables: variable-docs 87 | ) 88 | } 89 | 90 | 91 | /// Parse the doc-comments of a typst module. This function returns a dictionary 92 | /// with the keys 93 | /// - `name`: The module name as a string. 94 | /// - `functions`: A list of function documentations as dictionaries. 95 | /// - `label-prefix`: The prefix for internal labels and references. 96 | /// The label prefix will automatically be the name of the module if not given 97 | /// explicity. 98 | /// 99 | /// The function documentation dictionaries contain the keys 100 | /// - `name`: The function name. 101 | /// - `description`: The function's description. 102 | /// - `args`: A dictionary of info objects for each function argument. 103 | /// 104 | /// These again are dictionaries with the keys 105 | /// - `description` (optional): The description for the argument. 106 | /// - `types` (optional): A list of accepted argument types. 107 | /// - `default` (optional): Default value for this argument. 108 | /// 109 | /// See @show-module for outputting the results of this function. 110 | #let parse-module( 111 | 112 | /// Content of `.typ` file to analyze for docstrings. 113 | /// -> str 114 | content, 115 | 116 | /// The name for the module. 117 | /// -> str 118 | name: "", 119 | 120 | /// The label-prefix for internal function references. If `auto`, the 121 | /// label-prefix name will be the module name. 122 | /// -> auto | str 123 | label-prefix: auto, 124 | 125 | /// Require that all parameters of a functions are documented and fail 126 | /// if some are not. 127 | /// -> bool 128 | require-all-parameters: false, 129 | 130 | /// A dictionary of definitions that are then available in all function 131 | /// and parameter descriptions. 132 | /// -> dictionary 133 | scope: (:), 134 | 135 | /// Code to prepend to all code snippets shown with `#example()`. 136 | /// This can for instance be used to import something from the scope. 137 | /// -> str 138 | preamble: "", 139 | 140 | /// Whether to enable the detection of curried functions. 141 | /// -> bool 142 | enable-curried-functions: true, 143 | 144 | /// Whether to use the old documentation syntax. 145 | /// -> bool 146 | old-syntax: false 147 | ) = { 148 | if label-prefix == auto { label-prefix = name + "-" } 149 | 150 | let docs = ( 151 | name: name, 152 | label-prefix: label-prefix, 153 | scope: scope, 154 | preamble: preamble 155 | ) 156 | if old-syntax { 157 | docs += old-parse(content, require-all-parameters: require-all-parameters, label-prefix: label-prefix, enable-curried-functions: enable-curried-functions) 158 | } else { 159 | docs += new-parser.parse(content) 160 | } 161 | // TODO 162 | if enable-curried-functions { 163 | docs.functions = resolve-parents(docs.functions) 164 | } 165 | 166 | 167 | return docs 168 | } 169 | -------------------------------------------------------------------------------- /docs/migration-to-0.4.0.md: -------------------------------------------------------------------------------- 1 | # Migration guide from 0.3.0 to 0.4.0 (new parser) 2 | 3 | 4 | If you choose to use the new documentation parser, this guide helps you with migrating your existing documentation to the new documentation syntax. Of course, you can also keep using the old syntax which will still be around for some time. It can be activated via `tidy.parse-module(old-syntax: true, ...)`. 5 | 6 | ## Breaking changes 7 | 8 | Below you can find an overview over the breaking changes that the new syntax introduces. 9 | 10 | - [Documentation of function arguments](#documentation-of-function-arguments) 11 | - [Cross-references](#cross-references) 12 | - [Example previews](#example-previews) (not strictly a breaking change) 13 | - [Doc-comment tests](#doc-comment-tests) 14 | 15 | 16 | ### Documentation of function arguments 17 | 18 | In Tidy 0.3.0 and earlier, function arguments were all documented in a dedicated block that was part of the description doc-comment for the function, see below: 19 | ```typ 20 | /// This function computes the cardinal sine, $sinc(x)=sin(x)/x$. 21 | /// 22 | /// - x (int, float): The argument for the cardinal sine function. 23 | /// -> float 24 | #let sinc(x) = if x == 0 {1} else {calc.sin(x) / x} 25 | ``` 26 | 27 | With the new syntax, the parameter description is moved right in front of the parameter declaration. The name of the parameter can thus be removed from the description. The type, however, is now annotated (until Typst provides built-in support for type annotations) just like the return type of the function itself with a trailing `->` expression. Also, multiple accepted types should now be separated with a pipe `|` operator instead of a comma (this also applies to the function return type). 28 | 29 | The previous example thus becomes 30 | 31 | ```typ 32 | /// This function computes the cardinal sine, $sinc(x)=sin(x)/x$. 33 | /// 34 | /// -> float 35 | #let sinc( 36 | /// The argument for the cardinal sine function. 37 | /// -> int | float 38 | x 39 | ) = if x == 0 {1} else {calc.sin(x) / x} 40 | ``` 41 | Note that the trailing type annotation does not need to be on its own line. 42 | 43 | 44 | ### Cross-references 45 | 46 | In the old documentation style, there was a dedicated syntax for cross-references: the `@@` prefix. The new style uses just plain Typst references starting with a single `@`. In the case of cross-referencing a function, parentheses are never placed after the function name. *This is a breaking change to before when these parentheses were obligatory*. 47 | 48 | In addition, it is now possible to reference a specific parameter of a function by appending a dot `.` and the parameter name, e.g., `sinc.x`. In order to use parameter references, the utilized template style needs to support them by creating appropriate labels for each parameter. The built-in style templates all support parameter referencing out of the box. 49 | 50 | 51 | ### Example previews 52 | 53 | A popular feature of Tidy is the example preview. A function, variable, or parameter description can contain demonstrating code examples that are automatically rendered and displayed side-by-side with the code. This used to be achieved through the `example()` function that Tidy provides since version 0.3.0. 54 | 55 | Although this function is still available, we now encourage users to use a raw element with the language `example` (for Typst markdown mode) or `examplec` (for Typst code mode). 56 | 57 | Thus, instead of 58 | ````typ 59 | /// This function computes the cardinal sine, $sinc(x)=sin(x)/x$. 60 | /// 61 | /// #example(`#sinc(0)`, mode: "markup") 62 | .. 63 | ```` 64 | we can now simply write 65 | 66 | ````typ 67 | /// This function computes the cardinal sine, $sinc(x)=sin(x)/x$. 68 | /// 69 | /// ```example 70 | /// #sinc(0) 71 | /// ``` 72 | .. 73 | ```` 74 | or 75 | ````typ 76 | /// This function computes the cardinal sine, $sinc(x)=sin(x)/x$. 77 | /// 78 | /// ```examplec 79 | /// sinc(0) 80 | /// ``` 81 | .. 82 | ```` 83 | 84 | In all versions, you can insert _hidden_ code lines starting with `>>>` anywhere in the demo code. These lines will just be executed but not displayed. 85 | ````typ 86 | /// ```examplec 87 | /// >>> import my-math: sinc // just executed, not shown 88 | /// sinc(0) 89 | /// ``` 90 | ```` 91 | This is useful for many scenarios like import statements, wrapping everything inside a container of a fixed size and other things. 92 | 93 | Look at the [default.typ](/src/styles/default.typ) template style for hints on customization of the example preview. 94 | 95 | 96 | ## Standalone usage of example previews 97 | 98 | Some people use the example preview feature to add self-compiling code examples independently of Tidy. This used to be possible via the following show rule: 99 | ````typ 100 | #show raw: show-example.show-example 101 | 102 | ```typ 103 | Hello world 104 | ``` 105 | ```` 106 | With the new version, this should be replaced with 107 | ```typ 108 | #import "@preview/tidy:0.4.3": render-examples 109 | #show: render-examples 110 | 111 | ... 112 | ``` 113 | 114 | ### Scope 115 | It also features a `scope` argument, that can be pre-set: 116 | 117 | ````typ 118 | #show: render-examples.with(scope: (answer: 42)) 119 | 120 | ```example 121 | #answer 122 | ``` 123 | ```` 124 | 125 | 126 | ### Customization 127 | The output format of the example can be customized through the parameter `layout` of `render-examples`. This parameter takes a `function` with two positional arguments: the `raw` element and the preview. 128 | ````typ 129 | #show: render-examples.with(layout: (code, preview) => grid(code, preview)) 130 | ```` 131 | 132 | ## Doc-comment tests 133 | Doc-comment tests can still be used as before but the short-hand syntax with `>>>` is no longer supported with the new documentation syntax. With `old-parser: true`, it is still available. -------------------------------------------------------------------------------- /tests/test_parse.typ: -------------------------------------------------------------------------------- 1 | #import "/src/tidy.typ": * 2 | #import "/src/old-parser.typ": * 3 | #import "/src/utilities.typ": * 4 | 5 | #let eval-string(string) = eval-docstring(string, (scope: (:))) 6 | 7 | #let parse-module = parse-module.with(old-syntax: true) 8 | 9 | #{ 10 | let code = ``` 11 | /// - alpha (str): 12 | /// - beta (length): 13 | // / - ..children (any): 14 | #let z(alpha, beta: 2pt, ..children) = {} 15 | ``` 16 | let k = parse-module(code.text) 17 | } 18 | 19 | // Test reference-matcher 20 | #{ 21 | let matches = " @@func".matches(reference-matcher) 22 | assert.eq(matches.len(), 1) 23 | assert.eq(matches.at(0).captures, ("func",)) 24 | 25 | let matches = " @@func()".matches(reference-matcher) 26 | assert.eq(matches.len(), 1) 27 | assert.eq(matches.at(0).captures, ("func()",)) 28 | 29 | let matches = " ()@@@@my-func12-bliblablub @@ @@a".matches(reference-matcher) 30 | assert.eq(matches.len(), 2) 31 | assert.eq(matches.at(0).captures, ("my-func12-bliblablub",)) 32 | assert.eq(matches.at(1).captures, ("a",)) 33 | } 34 | 35 | 36 | // Test argument-documentation-matcher 37 | #{ 38 | let matches = " \t\n\t /// - my-arg1 (string, content): desc".matches(argument-documentation-matcher) 39 | assert.eq(matches.len(), 1) 40 | assert.eq(matches.at(0).captures, ("my-arg1","string, content", "desc")) 41 | 42 | // multiline argument description 43 | let matches = "/// - arg (type): desc\n\tasd\n-3$234$".matches(argument-documentation-matcher) 44 | assert.eq(matches.len(), 1) 45 | assert.eq(matches.at(0).captures, ("arg", "type", "desc")) 46 | } 47 | 48 | 49 | // Basic tests 50 | #{ 51 | let a = ``` 52 | /// Func 53 | #let a-3_56C() = {} 54 | ```.text 55 | let result = parse-module(a) 56 | assert.eq(result.functions.len(), 1) 57 | assert.eq(result.functions.at(0).name, "a-3_56C") 58 | assert.eq(eval-string(result.functions.at(0).description), [Func]) 59 | assert.eq(result.functions.at(0).return-types, none) 60 | } 61 | 62 | 63 | #{ 64 | let a = ``` 65 | #{ 66 | /// Func 67 | /// 68 | let a() = {} 69 | } 70 | ```.text 71 | let result = parse-module(a) 72 | assert.eq(result.functions.len(), 1) 73 | assert.eq(result.functions.at(0).name, "a") 74 | assert.eq(eval-string(result.functions.at(0).description), [Func]) 75 | assert.eq(result.functions.at(0).return-types, none) 76 | } 77 | 78 | 79 | 80 | // Parameters and defaults 81 | #{ 82 | let a = ``` 83 | /// Func 84 | #let a(p1, p2: 2, p3: (), p4: ("entries": ())) = {} 85 | ```.text 86 | let result = parse-module(a) 87 | assert.eq(result.functions.len(), 1) 88 | let f0 = result.functions.at(0) 89 | 90 | assert.eq(f0.name, "a") 91 | assert.eq(eval-string(f0.description), [Func]) 92 | assert.eq(f0.args.len(), 4) 93 | assert.eq(f0.args.p1, (:)) 94 | assert.eq(f0.args.p2, (default: "2")) 95 | assert.eq(f0.args.p3, (default: "()")) 96 | assert.eq(f0.args.p4, (default: "(\"entries\": ())")) 97 | assert.eq(f0.return-types, none) 98 | } 99 | 100 | 101 | 102 | 103 | // Parameter and return types 104 | #{ 105 | let a = ``` 106 | /// Func 107 | /// - p1 (string): a param $a$ 108 | /// - p2 (bool, function): a param $b$ 109 | /// Oh yes 110 | /// - p3 (string): 111 | /// -> content, integer 112 | #let a(p1, p2: 2, p3: (), p4: ("entries": ())) = {} 113 | ```.text 114 | let result = parse-module(a) 115 | assert.eq(result.functions.len(), 1) 116 | let f0 = result.functions.at(0) 117 | 118 | assert.eq(f0.name, "a") 119 | assert.eq(eval-string(f0.description), [Func]) 120 | assert.eq(f0.args.len(), 4) 121 | assert.eq(f0.args.p1.types, ("string",)) 122 | assert.eq(eval-string(f0.args.p1.description), [a param $a$]) 123 | assert.eq(f0.args.p2.default, "2") 124 | assert.eq(eval-string(f0.args.p2.description), [a param $b$ Oh yes]) 125 | assert.eq(f0.args.p2.types, ("bool", "function")) 126 | assert.eq(f0.return-types, ("content", "integer")) 127 | } 128 | 129 | 130 | 131 | 132 | 133 | // // Ignore args that are not in the argument list 134 | // #{ 135 | // let a = ``` 136 | // /// Func 137 | // /// - bar (content): asd 138 | // #let a(bar) = {} 139 | // ```.text 140 | // let result = parse-module(a) 141 | // assert.eq(result.functions.len(), 1) 142 | // assert.eq(result.functions.at(0).args.len(), 1) 143 | // } 144 | 145 | 146 | // Ignore interrupted doc-comment 147 | #{ 148 | let a = ``` 149 | /// Func 150 | // a 151 | #let a() 152 | ```.text 153 | let result = parse-module(a) 154 | assert.eq(result.functions.len(), 0) 155 | } 156 | 157 | 158 | // Ensure that wide unicode characters don't disturb line calculation for error messages 159 | #{ 160 | let code = ``` 161 | // ⇒⇐ßáà€ 162 | 163 | /// foo 164 | /// >>> 2 == 2 165 | #let f() 166 | ``` 167 | 168 | let result = show-module(parse-module(code.text)) 169 | } 170 | 171 | // Curried functions 172 | #{ 173 | let code = ``` 174 | /// - foo (content): Something. 175 | /// - bar (bool): A bool. 176 | /// -> content 177 | #let myfunc(foo, bar: false) = strong(foo) 178 | 179 | /// My curried function. 180 | /// -> content 181 | #let curried = myfunc.with(bar: true, 2) 182 | ``` 183 | 184 | let result = parse-module(code.text) 185 | let f1 = result.functions.at(0) 186 | let f2 = result.functions.at(1) 187 | assert.eq(f1.name, "myfunc") 188 | assert.eq(f2.parent.name, "myfunc") 189 | assert.eq(f2.parent.pos, ("2",)) 190 | assert.eq(f2.parent.named, (bar: "true")) 191 | // assert.eq(f2.args.len(), 1) 192 | // assert.eq(f2.args.bar, ( 193 | // default: "true", 194 | // description: "A bool.", 195 | // types: ("bool",), 196 | // )) 197 | } 198 | 199 | // module description 200 | #{ 201 | let code = ``` 202 | // License 203 | 204 | /// This is a module 205 | 206 | a 207 | ``` 208 | 209 | let result = parse-module(code.text) 210 | assert.eq(result.description, "This is a module") 211 | } -------------------------------------------------------------------------------- /tests/old-parser/parse-module.typ: -------------------------------------------------------------------------------- 1 | #import "/src/tidy.typ": * 2 | #import "/src/old-parser.typ": * 3 | #import "/src/utilities.typ": * 4 | 5 | #let eval-string(string) = eval-docstring(string, (scope: (:))) 6 | 7 | #let parse-module = parse-module.with(old-syntax: true) 8 | 9 | #{ 10 | let code = ``` 11 | /// - alpha (str): 12 | /// - beta (length): 13 | // / - ..children (any): 14 | #let z(alpha, beta: 2pt, ..children) = {} 15 | ``` 16 | let k = parse-module(code.text) 17 | } 18 | 19 | // Test reference-matcher 20 | #{ 21 | let matches = " @@func".matches(reference-matcher) 22 | assert.eq(matches.len(), 1) 23 | assert.eq(matches.at(0).captures, ("func",)) 24 | 25 | let matches = " @@func()".matches(reference-matcher) 26 | assert.eq(matches.len(), 1) 27 | assert.eq(matches.at(0).captures, ("func()",)) 28 | 29 | let matches = " ()@@@@my-func12-bliblablub @@ @@a".matches(reference-matcher) 30 | assert.eq(matches.len(), 2) 31 | assert.eq(matches.at(0).captures, ("my-func12-bliblablub",)) 32 | assert.eq(matches.at(1).captures, ("a",)) 33 | } 34 | 35 | 36 | // Test argument-documentation-matcher 37 | #{ 38 | let matches = " \t\n\t /// - my-arg1 (string, content): desc".matches(argument-documentation-matcher) 39 | assert.eq(matches.len(), 1) 40 | assert.eq(matches.at(0).captures, ("my-arg1","string, content", "desc")) 41 | 42 | // multiline argument description 43 | let matches = "/// - arg (type): desc\n\tasd\n-3$234$".matches(argument-documentation-matcher) 44 | assert.eq(matches.len(), 1) 45 | assert.eq(matches.at(0).captures, ("arg", "type", "desc")) 46 | } 47 | 48 | 49 | // Basic tests 50 | #{ 51 | let a = ``` 52 | /// Func 53 | #let a-3_56C() = {} 54 | ```.text 55 | let result = parse-module(a) 56 | assert.eq(result.functions.len(), 1) 57 | assert.eq(result.functions.at(0).name, "a-3_56C") 58 | assert.eq(eval-string(result.functions.at(0).description), [Func]) 59 | assert.eq(result.functions.at(0).return-types, none) 60 | } 61 | 62 | 63 | #{ 64 | let a = ``` 65 | #{ 66 | /// Func 67 | /// 68 | let a() = {} 69 | } 70 | ```.text 71 | let result = parse-module(a) 72 | assert.eq(result.functions.len(), 1) 73 | assert.eq(result.functions.at(0).name, "a") 74 | assert.eq(eval-string(result.functions.at(0).description), [Func]) 75 | assert.eq(result.functions.at(0).return-types, none) 76 | } 77 | 78 | 79 | 80 | // Parameters and defaults 81 | #{ 82 | let a = ``` 83 | /// Func 84 | #let a(p1, p2: 2, p3: (), p4: ("entries": ())) = {} 85 | ```.text 86 | let result = parse-module(a) 87 | assert.eq(result.functions.len(), 1) 88 | let f0 = result.functions.at(0) 89 | 90 | assert.eq(f0.name, "a") 91 | assert.eq(eval-string(f0.description), [Func]) 92 | assert.eq(f0.args.len(), 4) 93 | assert.eq(f0.args.p1, (:)) 94 | assert.eq(f0.args.p2, (default: "2")) 95 | assert.eq(f0.args.p3, (default: "()")) 96 | assert.eq(f0.args.p4, (default: "(\"entries\": ())")) 97 | assert.eq(f0.return-types, none) 98 | } 99 | 100 | 101 | 102 | 103 | // Parameter and return types 104 | #{ 105 | let a = ``` 106 | /// Func 107 | /// - p1 (string): a param $a$ 108 | /// - p2 (bool, function): a param $b$ 109 | /// Oh yes 110 | /// - p3 (string): 111 | /// -> content, integer 112 | #let a(p1, p2: 2, p3: (), p4: ("entries": ())) = {} 113 | ```.text 114 | let result = parse-module(a) 115 | assert.eq(result.functions.len(), 1) 116 | let f0 = result.functions.at(0) 117 | 118 | assert.eq(f0.name, "a") 119 | assert.eq(eval-string(f0.description), [Func]) 120 | assert.eq(f0.args.len(), 4) 121 | assert.eq(f0.args.p1.types, ("string",)) 122 | assert.eq(eval-string(f0.args.p1.description), [a param $a$]) 123 | assert.eq(f0.args.p2.default, "2") 124 | assert.eq(eval-string(f0.args.p2.description), [a param $b$ Oh yes]) 125 | assert.eq(f0.args.p2.types, ("bool", "function")) 126 | assert.eq(f0.return-types, ("content", "integer")) 127 | } 128 | 129 | 130 | 131 | 132 | 133 | // // Ignore args that are not in the argument list 134 | // #{ 135 | // let a = ``` 136 | // /// Func 137 | // /// - bar (content): asd 138 | // #let a(bar) = {} 139 | // ```.text 140 | // let result = parse-module(a) 141 | // assert.eq(result.functions.len(), 1) 142 | // assert.eq(result.functions.at(0).args.len(), 1) 143 | // } 144 | 145 | 146 | // Ignore interrupted doc-comment 147 | #{ 148 | let a = ``` 149 | /// Func 150 | // a 151 | #let a() 152 | ```.text 153 | let result = parse-module(a) 154 | assert.eq(result.functions.len(), 0) 155 | } 156 | 157 | 158 | // Ensure that wide unicode characters don't disturb line calculation for error messages 159 | #{ 160 | let code = ``` 161 | // ⇒⇐ßáà€ 162 | 163 | /// foo 164 | /// >>> 2 == 2 165 | #let f() 166 | ``` 167 | 168 | let result = show-module(parse-module(code.text)) 169 | } 170 | 171 | // Curried functions 172 | #{ 173 | let code = ``` 174 | /// - foo (content): Something. 175 | /// - bar (bool): A bool. 176 | /// -> content 177 | #let myfunc(foo, bar: false) = strong(foo) 178 | 179 | /// My curried function. 180 | /// -> content 181 | #let curried = myfunc.with(bar: true, 2) 182 | ``` 183 | 184 | let result = parse-module(code.text) 185 | let f1 = result.functions.at(0) 186 | let f2 = result.functions.at(1) 187 | assert.eq(f1.name, "myfunc") 188 | assert.eq(f2.parent.name, "myfunc") 189 | assert.eq(f2.parent.pos, ("2",)) 190 | assert.eq(f2.parent.named, (bar: "true")) 191 | // assert.eq(f2.args.len(), 1) 192 | // assert.eq(f2.args.bar, ( 193 | // default: "true", 194 | // description: "A bool.", 195 | // types: ("bool",), 196 | // )) 197 | } 198 | 199 | // module description 200 | #{ 201 | let code = ``` 202 | // License 203 | 204 | /// This is a module 205 | 206 | a 207 | ``` 208 | 209 | let result = parse-module(code.text) 210 | assert.eq(result.description, "This is a module") 211 | } -------------------------------------------------------------------------------- /src/styles/minimal.typ: -------------------------------------------------------------------------------- 1 | #import "../utilities.typ": * 2 | 3 | 4 | // Color to highlight function names in 5 | #let fn-color = rgb("#1f2a63") 6 | 7 | #let get-type-color(type) = rgb("#eff0f3") 8 | 9 | 10 | #let show-outline(module-doc, style-args: (:)) = { 11 | let prefix = module-doc.label-prefix 12 | let gen-entry(name) = { 13 | if style-args.enable-cross-references { 14 | link(label(prefix + name), name) 15 | } else { 16 | name 17 | } 18 | } 19 | if module-doc.functions.len() > 0 { 20 | list(..module-doc.functions.map(fn => gen-entry(fn.name + "()"))) 21 | } 22 | 23 | if module-doc.variables.len() > 0 { 24 | text( 25 | get-local-name("variables", style-args: style-args), 26 | weight: "bold" 27 | ) 28 | list(..module-doc.variables.map(var => gen-entry(var.name))) 29 | } 30 | } 31 | 32 | // Create beautiful, colored type box 33 | #let show-type(type, style-args: (:)) = { 34 | h(2pt) 35 | box(outset: 2pt, fill: get-type-color(type), radius: 2pt, raw(type, lang: none)) 36 | h(2pt) 37 | } 38 | 39 | 40 | 41 | #let show-parameter-list(fn, style-args) = { 42 | block(fill: rgb("#d8dbed"), width: 100%, inset: (x: 0.5em, y: 0.7em), { 43 | set text(font: "Cascadia Mono", size: 0.85em, weight: 340) 44 | text(fn.name, fill: fn-color) 45 | "(" 46 | let inline-args = fn.args.len() < 5 47 | if not inline-args { "\n " } 48 | let items = () 49 | for (name, info) in fn.args { 50 | if style-args.omit-private-parameters and name.starts-with("_") { 51 | continue 52 | } 53 | let types 54 | if "types" in info { 55 | types = ": " + info.types.map(x => show-type(x)).join(" ") 56 | } 57 | if style-args.enable-cross-references and not (info.at("description", default: "") == "" and style-args.omit-empty-param-descriptions) { 58 | name = link(label(style-args.label-prefix + fn.name + "." + name.trim(".")), name) 59 | } 60 | items.push(box(name + types)) 61 | } 62 | items.join( if inline-args {", "} else { ",\n "}) 63 | if not inline-args { "\n" } + ")" 64 | if fn.return-types != none { 65 | box[~-> #fn.return-types.map(x => show-type(x)).join(" ")] 66 | } 67 | }) 68 | } 69 | 70 | 71 | 72 | // Create a parameter description block, containing name, type, description and optionally the default value. 73 | #let show-parameter-block( 74 | name, types, content, style-args, 75 | show-default: false, 76 | default: none, 77 | function-name: none 78 | ) = block( 79 | inset: 0pt, width: 100%, 80 | breakable: style-args.break-param-descriptions, 81 | [ 82 | #[ 83 | #set text(fill: fn-color) 84 | #raw(name, lang: none) 85 | #if function-name != none and style-args.enable-cross-references { label(function-name + "." + name.trim(".")) } 86 | ] 87 | (#h(-.2em) 88 | #types.map(x => (style-args.style.show-type)(x)).join([ #text("or",size:.6em) ]) 89 | #if show-default [\= #raw(lang: "typc", default) ] 90 | #h(-.2em)) -- 91 | #content 92 | 93 | ] 94 | ) 95 | 96 | 97 | #let show-function( 98 | fn, style-args, 99 | ) = { 100 | set par(justify: false, hanging-indent: 1em, first-line-indent: 0em) 101 | 102 | block(breakable: style-args.break-param-descriptions)[ 103 | #(style-args.style.show-parameter-list)(fn, style-args) 104 | #if style-args.enable-cross-references { 105 | label(style-args.label-prefix + fn.name + "()") 106 | } 107 | ] 108 | pad(x: 0em, eval-docstring(fn.description, style-args)) 109 | 110 | let parameter-block 111 | 112 | for (name, info) in fn.args { 113 | if style-args.omit-private-parameters and name.starts-with("_") { 114 | continue 115 | } 116 | let types = info.at("types", default: ()) 117 | let description = info.at("description", default: "") 118 | if description == "" and style-args.omit-empty-param-descriptions { continue } 119 | parameter-block += (style-args.style.show-parameter-block)( 120 | name, types, eval-docstring(description, style-args), 121 | style-args, 122 | show-default: "default" in info, 123 | default: info.at("default", default: none), 124 | function-name: style-args.label-prefix + fn.name 125 | ) 126 | } 127 | 128 | if parameter-block != none { 129 | [*#get-local-name("parameters", style-args: style-args):*] 130 | parameter-block 131 | } 132 | v(4em, weak: true) 133 | } 134 | 135 | 136 | #let show-variable( 137 | var, style-args, 138 | ) = { 139 | set par(justify: false, hanging-indent: 1em, first-line-indent: 0em) 140 | 141 | let type = if "type" not in var { none } 142 | else { show-type(var.type, style-args: style-args) } 143 | 144 | block(breakable: style-args.break-param-descriptions, fill: rgb("#d8dbed"), width: 100%, inset: (x: 0.5em, y: 0.7em), 145 | stack(dir: ltr, spacing: 1.2em, 146 | if style-args.enable-cross-references [ 147 | #set text(font: "DejaVu Sans Mono", size: 0.85em, weight: 340) 148 | #text(var.name, fill: fn-color) 149 | #label(style-args.label-prefix + var.name) 150 | ] else [ 151 | #set text(font: "DejaVu Sans Mono", size: 0.85em, weight: 340) 152 | #text(var.name, fill: fn-color) 153 | ], 154 | type 155 | ) 156 | ) 157 | pad(x: 0em, eval-docstring(var.description, style-args)) 158 | 159 | v(4em, weak: true) 160 | } 161 | 162 | 163 | #let show-reference(label, name, style-args: none) = { 164 | link(label, raw(name, lang: none)) 165 | } 166 | 167 | #import "../show-example.typ" as example 168 | 169 | #let show-example( 170 | ..args 171 | ) = { 172 | 173 | example.show-example( 174 | ..args, 175 | layout: example.default-layout-example.with( 176 | code-block: block.with(stroke: .5pt + fn-color), 177 | preview-block: block.with(stroke: .5pt + fn-color), 178 | col-spacing: 0pt 179 | ), 180 | ) 181 | } -------------------------------------------------------------------------------- /src/show-module.typ: -------------------------------------------------------------------------------- 1 | #import "styles.typ" 2 | #import "utilities.typ" 3 | #import "testing.typ" 4 | 5 | 6 | #let def-state = state("tidy-definitions", (:)) 7 | 8 | 9 | /// Show given module in the given style. 10 | /// This displays all (documented) functions in the module. 11 | /// 12 | /// -> content 13 | #let show-module( 14 | 15 | /// Module documentation information as returned by @parse-module. 16 | /// -> dictionary 17 | module-doc, 18 | 19 | /// The output style to use. This can be a module 20 | /// defining the functions `show-outline`, `show-type`, `show-function`, 21 | /// `show-parameter-list` and `show-parameter-block` or a dictionary with 22 | /// functions for the same keys. 23 | /// -> module | dictionary 24 | style: styles.default, 25 | 26 | /// Level for the module heading. Function names are created as second-level 27 | /// headings and the "Parameters" heading is two levels below the first 28 | /// heading level. 29 | /// -> int 30 | first-heading-level: 2, 31 | 32 | /// Whether to output the name of the module at the top. 33 | /// -> bool 34 | show-module-name: true, 35 | 36 | /// Whether to allow breaking of parameter description blocks. 37 | /// -> bool 38 | break-param-descriptions: false, 39 | 40 | /// Whether to omit description blocks for parameters with empty description. 41 | /// -> bool 42 | omit-empty-param-descriptions: true, 43 | 44 | /// Whether to omit functions and variables starting with an underscore. 45 | /// -> bool 46 | omit-private-definitions: false, 47 | 48 | /// Whether to omit named function arguments starting with an underscore. 49 | /// -> bool 50 | omit-private-parameters: false, 51 | 52 | /// Whether to output an outline of all functions in the module at the beginning. 53 | /// -> bool 54 | show-outline: true, 55 | 56 | /// Function to use to sort the function documentations. With `auto`, they are 57 | /// sorted alphabetically by name and with `none` they are not sorted. Otherwise 58 | /// a function can be passed that each function documentation object is passed to 59 | /// and that should return some key to sort the functions by. 60 | /// -> auto | none | function 61 | sort-functions: auto, 62 | 63 | /// Whether to run doc-comment tests. 64 | /// -> bool 65 | enable-tests: true, 66 | 67 | /// Whether to enable links for cross-references. If set to auto, the style 68 | /// will select its default color set. 69 | /// -> bool 70 | enable-cross-references: true, 71 | 72 | /// Give a dictionary for type and colors and other colors. 73 | /// -> auto | dictionary 74 | colors: auto, 75 | 76 | /// Language-specific names for strings used in the output. Currently, these 77 | /// are `parameters` and `default`. You can for example use: 78 | /// `local-names: (parameters: [Parameter], default: [Standard], variables: [Variablen])`. 79 | /// If set to `auto`, automatic translations will be used according to the 80 | /// current document language. 81 | /// -> dictionary 82 | local-names: auto 83 | 84 | ) = block({ 85 | let label-prefix = module-doc.label-prefix 86 | if sort-functions == auto { 87 | module-doc.functions = module-doc.functions.sorted(key: x => x.name) 88 | } else if type(sort-functions) == function { 89 | module-doc.functions = module-doc.functions.sorted(key: sort-functions) 90 | } 91 | 92 | if omit-private-definitions { 93 | let filter = x => not x.name.starts-with("_") 94 | module-doc.functions = module-doc.functions.filter(filter) 95 | module-doc.variables = module-doc.variables.filter(filter) 96 | } 97 | 98 | let style-functions = utilities.get-style-functions(style) 99 | 100 | if local-names == auto { 101 | local-names = (:) 102 | } else { 103 | assert( 104 | type(local-names) == dictionary, 105 | message: "The parameter `local-names` expects a dictionary of translations. " 106 | ) 107 | } 108 | 109 | let style-args = ( 110 | style: style-functions, 111 | label-prefix: label-prefix, 112 | first-heading-level: first-heading-level, 113 | break-param-descriptions: break-param-descriptions, 114 | omit-empty-param-descriptions: omit-empty-param-descriptions, 115 | omit-private-parameters: omit-private-parameters, 116 | colors: colors, 117 | enable-cross-references: enable-cross-references, 118 | local-names: local-names, 119 | ) 120 | 121 | let eval-scope = ( 122 | // Predefined functions that may be called by the user in doc-comment code 123 | example: style-functions.show-example.with( 124 | inherited-scope: module-doc.scope, 125 | preamble: module-doc.preamble 126 | ), 127 | test: testing.test.with( 128 | inherited-scope: testing.assertations + module-doc.scope, 129 | enable: enable-tests 130 | ), 131 | // Internally generated functions 132 | tidy: ( 133 | show-reference: style-functions.show-reference.with(style-args: style-args) 134 | ) 135 | ) 136 | 137 | eval-scope += module-doc.scope 138 | 139 | style-args.scope = eval-scope 140 | 141 | def-state.update(x => { 142 | x + module-doc.functions.map(x => (x.name, 1)).to-dict() + module-doc.variables.map(x => (x.name, 0)).to-dict() 143 | }) 144 | 145 | show ref: it => { 146 | let target = str(it.target) 147 | if target.starts-with(label-prefix){ return it } 148 | if not enable-cross-references { 149 | return raw(target) 150 | } 151 | let defs = def-state.final() 152 | 153 | let base = target 154 | if "." in base { base = base.split(".").first() } 155 | let target-def = defs.at(target, default: none) 156 | let base-def = defs.at(base, default: none) 157 | if target-def == none and base-def == none { return it } 158 | 159 | if target-def == 1 { 160 | target += "()" 161 | } 162 | (eval-scope.tidy.show-reference)(label(label-prefix + target), target) 163 | } 164 | 165 | show raw.where(lang: "example"): it => { 166 | set text(4em / 3) 167 | 168 | (eval-scope.example)( 169 | raw(it.text, block: true, lang: "typ"), 170 | mode: "markup" 171 | ) 172 | } 173 | show raw.where(lang: "examplec"): it => { 174 | set text(4em / 3) 175 | 176 | (eval-scope.example)( 177 | raw(it.text, block: true, lang: "typc"), 178 | mode: "code" 179 | ) 180 | } 181 | 182 | // Show the docs 183 | 184 | if "name" in module-doc and show-module-name and module-doc.name != "" { 185 | heading(module-doc.name, level: first-heading-level) 186 | parbreak() 187 | } 188 | 189 | if show-outline { 190 | (style-functions.show-outline)(module-doc, style-args: style-args) 191 | } 192 | 193 | for (index, fn) in module-doc.functions.enumerate() { 194 | (style-functions.show-function)(fn, style-args) 195 | } 196 | for (index, fn) in module-doc.variables.enumerate() { 197 | (style-functions.show-variable)(fn, style-args) 198 | } 199 | }) 200 | 201 | 202 | -------------------------------------------------------------------------------- /src/show-example.typ: -------------------------------------------------------------------------------- 1 | 2 | /// Default example layouter used with @show-example. 3 | #let default-layout-example( 4 | /// Code `raw` element to display. 5 | /// -> raw 6 | code, 7 | 8 | /// Rendered preview. 9 | /// -> content 10 | preview, 11 | 12 | /// Direction for laying out the code and preview boxes. 13 | /// -> direction 14 | dir: ltr, 15 | 16 | /// Configures the ratio of the widths of the code and preview boxes. 17 | /// -> int 18 | ratio: 1, 19 | 20 | /// How much to rescale the preview. If set to auto, the the preview is scaled to fit the box. 21 | /// -> auto | ratio 22 | scale-preview: auto, 23 | 24 | /// The code is passed to this function. Use this to customize how the code is shown. 25 | /// -> function 26 | code-block: block, 27 | 28 | /// The preview is passed to this function. Use this to customize how the preview is shown. 29 | /// -> function 30 | preview-block: block, 31 | 32 | /// Spacing between the code and preview boxes. 33 | /// -> length 34 | col-spacing: 5pt 35 | ) = { 36 | 37 | let preview-outer-padding = 5pt 38 | let preview-inner-padding = 5pt 39 | 40 | layout(size => context { 41 | let code-width 42 | let preview-width 43 | 44 | if dir.axis() == "vertical" { 45 | code-width = size.width 46 | preview-width = size.width 47 | } else { 48 | code-width = ratio / (ratio + 1) * size.width - 0.5 * col-spacing 49 | preview-width = size.width - code-width - col-spacing 50 | } 51 | 52 | 53 | 54 | let available-preview-width = preview-width - 2 * (preview-outer-padding + preview-inner-padding) 55 | 56 | let preview-size 57 | let scale-preview = scale-preview 58 | 59 | if scale-preview == auto { 60 | preview-size = measure(preview) 61 | assert(preview-size.width != 0pt, message: "The code example has a relative width. Please set `scale-preview` to a fixed ratio, e.g., `100%`") 62 | scale-preview = calc.min(1, available-preview-width / preview-size.width) * 100% 63 | } else { 64 | preview-size = measure(block(preview, width: available-preview-width / (scale-preview / 100%))) 65 | } 66 | 67 | set par(hanging-indent: 0pt) // this messes up some stuff in case someone sets it 68 | 69 | 70 | // We first measure this thing (code + preview) to find out which of the two has 71 | // the larger height. Then we can just set the height for both boxes. 72 | let arrangement(width: 100%, height: auto) = block(width: width, inset: 0pt, stack(dir: dir, spacing: col-spacing, 73 | code-block( 74 | width: code-width, 75 | height: height, 76 | inset: 5pt, 77 | { 78 | set text(size: .9em) 79 | set raw(block: true) 80 | code 81 | } 82 | ), 83 | preview-block( 84 | height: height, width: preview-width, 85 | inset: preview-outer-padding, 86 | box( 87 | width: 100%, 88 | height: if height == auto {auto} else {height - 2*preview-outer-padding}, 89 | fill: white, 90 | inset: preview-inner-padding, 91 | box( 92 | inset: 0pt, 93 | width: preview-size.width * (scale-preview / 100%), 94 | height: preview-size.height * (scale-preview / 100%), 95 | place(scale( 96 | scale-preview, 97 | origin: top + left, 98 | block(preview, height: preview-size.height, width: preview-size.width) 99 | )) 100 | ) 101 | ) 102 | ) 103 | )) 104 | let height = if dir.axis() == "vertical" { auto } 105 | else { measure(arrangement(width: size.width)).height } 106 | arrangement(height: height) 107 | }) 108 | } 109 | 110 | 111 | 112 | /// Takes a `raw` elements and both displays the code and previews the result of 113 | /// its evaluation. 114 | /// 115 | /// The code is by default shown in the language mode `lang: typc` (typst code) 116 | /// if no language has been specified. Code in typst markup lanugage (`lang: typ`) 117 | /// is automatically evaluated in markup mode. 118 | /// 119 | /// Lines in the raw code that start with `>>>` are removed from the outputted code 120 | /// but evaluated in the preview. 121 | /// 122 | /// Lines starting with `<<<` are displayed in the preview, but not evaluated. 123 | #let show-example( 124 | 125 | /// Raw object holding the example code. 126 | /// -> raw 127 | code, 128 | 129 | /// Additional definitions to make available in the evaluation of the preview. 130 | /// -> dictionary 131 | scope: (:), 132 | 133 | /// Code to prepend to the snippet. This can for example be used to configure imports. 134 | /// This is currently only supported in `markup` mode, see @show-example.mode. 135 | /// -> str 136 | preamble: "", 137 | 138 | /// Language mode. Can be `auto`, `"markup"`, or `"code"`. 139 | /// -> auto | str 140 | mode: auto, 141 | 142 | /// This parameter is only used internally. Definitions that are made available to the 143 | /// entire parsed module. 144 | /// -> dictionary 145 | inherited-scope: (:), 146 | 147 | /// Layout function which is passed to code, the preview and all other options, 148 | /// see @show-example.options. 149 | /// -> function 150 | layout: default-layout-example, 151 | 152 | /// Additional options to pass to the layout function. 153 | /// -> any 154 | ..options 155 | 156 | ) = { 157 | let displayed-code = code 158 | .text 159 | .split("\n") 160 | .filter(x => not x.starts-with(">>>")) 161 | .map(x => x.trim("<<<", at: start)) 162 | .join("\n") 163 | let executed-code = code 164 | .text 165 | .split("\n") 166 | .filter(x => not x.starts-with("<<<")) 167 | .map(x => x.trim(">>>", at: start)) 168 | .join("\n") 169 | 170 | let lang = if code.has("lang") { code.lang } else { auto } 171 | if mode == auto { 172 | if lang == "typ" { mode = "markup" } 173 | else if lang == "typc" { mode = "code" } 174 | else if lang == "typm" { mode = "math" } 175 | else if lang == auto { mode = "markup" } 176 | } 177 | if lang == auto { 178 | if mode == "markup" { lang = "typ" } 179 | if mode == "code" { lang = "typc" } 180 | if mode == "math" { lang = "typm" } 181 | } 182 | if mode == "code" { 183 | preamble = "" 184 | } 185 | assert(lang in ("typ", "typc", "typm"), message: "Previewing code only supports the languages \"typ\", \"typc\", and \"typm\"") 186 | 187 | layout( 188 | raw(displayed-code, lang: lang, block: true), 189 | [#eval(preamble + executed-code, mode: mode, scope: scope + inherited-scope)], 190 | ..options 191 | ) 192 | } 193 | 194 | 195 | 196 | /// Adds the two languages `example` and `examplec` to `raw` that can be used 197 | /// to render code examples side-by-side with an automatic preview. 198 | /// 199 | /// This function is intended to be used in a show rule 200 | /// ```typ 201 | /// #show: render-example 202 | /// ``` 203 | #let render-examples( 204 | /// Body to apply the show rule to. 205 | /// -> any 206 | body, 207 | 208 | /// Scope 209 | /// -> dictionary 210 | scope: (:), 211 | 212 | /// Layout function which is passed the code, the preview and all other options, 213 | /// see @show-example.options. 214 | /// -> function 215 | layout: default-layout-example 216 | ) = { 217 | show raw.where(lang: "example"): it => { 218 | set text(4em / 3) 219 | 220 | show-example( 221 | raw(it.text, block: true, lang: "typ"), 222 | mode: "markup", 223 | scope: scope, 224 | layout: layout, 225 | ) 226 | } 227 | show raw.where(lang: "examplec"): it => { 228 | set text(4em / 3) 229 | 230 | show-example( 231 | raw(it.text, block: true, lang: "typc"), 232 | mode: "code", 233 | scope: scope, 234 | layout: layout, 235 | ..args 236 | ) 237 | } 238 | body 239 | } 240 | -------------------------------------------------------------------------------- /src/styles/default.typ: -------------------------------------------------------------------------------- 1 | #import "../utilities.typ": * 2 | 3 | // Color to highlight function names in 4 | #let function-name-color = rgb("#4b69c6") 5 | #let rainbow-map = ((rgb("#7cd5ff"), 0%), (rgb("#a6fbca"), 33%),(rgb("#fff37c"), 66%), (rgb("#ffa49d"), 100%)) 6 | #let gradient-for-color-types = gradient.linear(angle: 7deg, ..rainbow-map) 7 | #let gradient-for-tiling = gradient.linear(angle: -45deg, rgb("#ffd2ec"), rgb("#c6feff")).sharp(2).repeat(5) 8 | 9 | #let default-type-color = rgb("#eff0f3") 10 | 11 | // Colors for Typst types 12 | #let colors = ( 13 | "default": default-type-color, 14 | "content": rgb("#a6ebe6"), 15 | "string": rgb("#d1ffe2"), 16 | "str": rgb("#d1ffe2"), 17 | "none": rgb("#ffcbc4"), 18 | "auto": rgb("#ffcbc4"), 19 | "bool": rgb("#ffedc1"), 20 | "boolean": rgb("#ffedc1"), 21 | "integer": rgb("#e7d9ff"), 22 | "int": rgb("#e7d9ff"), 23 | "float": rgb("#e7d9ff"), 24 | "ratio": rgb("#e7d9ff"), 25 | "length": rgb("#e7d9ff"), 26 | "angle": rgb("#e7d9ff"), 27 | "relative length": rgb("#e7d9ff"), 28 | "relative": rgb("#e7d9ff"), 29 | "fraction": rgb("#e7d9ff"), 30 | "symbol": default-type-color, 31 | "array": default-type-color, 32 | "dictionary": default-type-color, 33 | "arguments": default-type-color, 34 | "selector": default-type-color, 35 | "module": default-type-color, 36 | "stroke": default-type-color, 37 | "function": rgb("#f9dfff"), 38 | "color": gradient-for-color-types, 39 | "gradient": gradient-for-color-types, 40 | "tiling": gradient-for-tiling, 41 | "signature-func-name": rgb("#4b69c6"), 42 | ) 43 | 44 | 45 | 46 | #let colors-dark = { 47 | let k = (:) 48 | let darkify(clr) = clr.darken(30%).saturate(30%) 49 | for (key, value) in colors { 50 | if type(value) == color { 51 | value = darkify(value) 52 | } else if type(value) == gradient { 53 | let map = value.stops().map(((clr, stop)) => (darkify(clr), calc.round(stop/1%)*1%)) 54 | value = value.kind()(..map) 55 | } 56 | k.insert(key, value) 57 | } 58 | k.signature-func-name = rgb("#4b69c6").lighten(40%) 59 | k 60 | } 61 | 62 | 63 | 64 | 65 | #let show-outline(module-doc, style-args: (:)) = { 66 | let prefix = module-doc.label-prefix 67 | let gen-entry(name) = { 68 | if "enable-cross-references" in style-args and style-args.enable-cross-references { 69 | link(label(prefix + name), name) 70 | } else { 71 | name 72 | } 73 | } 74 | if module-doc.functions.len() > 0 { 75 | list(..module-doc.functions.map(fn => gen-entry(fn.name + "()"))) 76 | } 77 | 78 | if module-doc.variables.len() > 0 { 79 | text(get-local-name("variables", style-args: style-args), weight: "bold") 80 | list(..module-doc.variables.map(var => gen-entry(var.name))) 81 | } 82 | } 83 | 84 | // Create beautiful, colored type box 85 | #let show-type(type, style-args: (:)) = { 86 | h(2pt) 87 | let clr = style-args.colors.at(type, default: style-args.colors.at("default", default: default-type-color)) 88 | box(outset: 2pt, fill: clr, radius: 2pt, raw(type, lang: none)) 89 | h(2pt) 90 | } 91 | 92 | 93 | 94 | #let show-parameter-list(fn, style-args: (:)) = { 95 | pad(x: 10pt, { 96 | set text(font: ("DejaVu Sans Mono"), size: 0.85em, weight: 340) 97 | text(fn.name, fill: style-args.colors.at("signature-func-name", default: rgb("#4b69c6"))) 98 | "(" 99 | let inline-args = fn.args.len() < 2 100 | if not inline-args { "\n " } 101 | let items = () 102 | let args = fn.args 103 | for (name, info) in fn.args { 104 | if style-args.omit-private-parameters and name.starts-with("_") { 105 | continue 106 | } 107 | let types 108 | if "types" in info { 109 | types = ": " + info.types.map(x => show-type(x, style-args: style-args)).join(" ") 110 | } 111 | if style-args.enable-cross-references and not (info.at("description", default: "") == "" and style-args.omit-empty-param-descriptions) { 112 | name = link(label(style-args.label-prefix + fn.name + "." + name.trim(".")), name) 113 | } 114 | items.push(name + types) 115 | } 116 | items.join( if inline-args {", "} else { ",\n "}) 117 | if not inline-args { "\n" } + ")" 118 | if "return-types" in fn and fn.return-types != none { 119 | " -> " 120 | fn.return-types.map(x => show-type(x, style-args: style-args)).join(" ") 121 | } 122 | }) 123 | } 124 | 125 | 126 | 127 | // Create a parameter description block, containing name, type, description and optionally the default value. 128 | #let show-parameter-block( 129 | function-name: none, name, types, content, style-args, 130 | show-default: false, 131 | default: none, 132 | ) = block( 133 | inset: 10pt, fill: rgb("ddd3"), width: 100%, 134 | breakable: style-args.break-param-descriptions, 135 | [ 136 | #box(heading(level: style-args.first-heading-level + 3, name)) 137 | #if function-name != none and style-args.enable-cross-references { label(function-name + "." + name.trim(".")) } 138 | #h(1.2em) 139 | #types.map(x => (style-args.style.show-type)(x, style-args: style-args)).join([ #text("or",size:.6em) ]) 140 | 141 | #content 142 | #if show-default [ 143 | #parbreak() 144 | #get-local-name("default", style-args: style-args): #raw(lang: "typc", default) 145 | ] 146 | ] 147 | ) 148 | 149 | 150 | #let show-function( 151 | fn, style-args, 152 | ) = { 153 | 154 | if style-args.colors == auto { style-args.colors = colors } 155 | 156 | [ 157 | #heading(fn.name, level: style-args.first-heading-level + 1) 158 | #if style-args.enable-cross-references { 159 | label(style-args.label-prefix + fn.name + "()") 160 | } 161 | ] 162 | 163 | eval-docstring(fn.description, style-args) 164 | 165 | block(breakable: style-args.break-param-descriptions, { 166 | heading( 167 | get-local-name("parameters", style-args: style-args), 168 | level: style-args.first-heading-level + 2 169 | ) 170 | (style-args.style.show-parameter-list)(fn, style-args: style-args) 171 | }) 172 | 173 | for (name, info) in fn.args { 174 | if style-args.omit-private-parameters and name.starts-with("_") { 175 | continue 176 | } 177 | let types = info.at("types", default: ()) 178 | let description = info.at("description", default: "") 179 | if description == "" and style-args.omit-empty-param-descriptions { continue } 180 | (style-args.style.show-parameter-block)( 181 | name, types, eval-docstring(description, style-args), 182 | style-args, 183 | show-default: "default" in info, 184 | default: info.at("default", default: none), 185 | function-name: style-args.label-prefix + fn.name 186 | ) 187 | } 188 | v(4.8em, weak: true) 189 | } 190 | 191 | 192 | 193 | #let show-variable( 194 | var, style-args, 195 | ) = { 196 | if style-args.colors == auto { style-args.colors = colors } 197 | let type = if "type" not in var { none } 198 | else { show-type(var.type, style-args: style-args) } 199 | 200 | stack(dir: ltr, spacing: 1.2em, 201 | if style-args.enable-cross-references [ 202 | #heading(var.name, level: style-args.first-heading-level + 1) 203 | #label(style-args.label-prefix + var.name) 204 | ] else [ 205 | #heading(var.name, level: style-args.first-heading-level + 1) 206 | ], 207 | type 208 | ) 209 | 210 | eval-docstring(var.description, style-args) 211 | v(4.8em, weak: true) 212 | } 213 | 214 | 215 | #let show-reference(label, name, style-args: none) = { 216 | link(label, raw(name, lang: none)) 217 | } 218 | 219 | 220 | #import "../show-example.typ" as example 221 | 222 | #let show-example( 223 | ..args 224 | ) = { 225 | 226 | example.show-example( 227 | ..args, 228 | layout: example.default-layout-example.with( 229 | code-block: block.with(radius: 3pt, stroke: .5pt + luma(200)), 230 | preview-block: block.with(radius: 3pt, fill: rgb("#e4e5ea")), 231 | col-spacing: 5pt 232 | ), 233 | ) 234 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Tidy 3 | *Keep it tidy.* 4 | 5 | [![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FMc-Zen%2Ftidy%2Fv0.4.3%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://typst.app/universe/package/tidy) 6 | [![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Mc-Zen/tidy/blob/main/LICENSE) 7 | [![Test Status](https://github.com/Mc-Zen/tidy/actions/workflows/run_tests.yml/badge.svg)](https://github.com/Mc-Zen/tidy/actions/workflows/run_tests.yml) 8 | [![User Manual](https://img.shields.io/badge/manual-.pdf-purple)][guide] 9 | 10 | 11 | 12 | 13 | **tidy** is a package that generates documentation directly in [Typst](https://typst.app/) for your Typst modules. It parses doc-comments and can be used to easily build a reference section for a module. Doc-comments use Typst syntax − so markup, equations and even figures are no problem! 14 | 15 | > [!IMPORTANT] 16 | > In version 0.4.0, the default documentation syntax has changed. You can take a look at the [migration guide][migration guide] or revert to the old syntax with `tidy.parse-module(old-syntax: true, ...)`. 17 | > 18 | > You can still find the documentation for the old syntax in the [0.3.0 user guide](https://github.com/Mc-Zen/tidy/releases/download/v0.3.0/tidy-guide.pdf). 19 | 20 | Features: 21 | - **Customizable** output styles. 22 | - Automatically [**preview code examples**](#example). 23 | - **Annotate types** of parameters and return values. 24 | - **Cross-references** to definitions and function parameters. 25 | - Automatically read off default values for named parameters. 26 | - [**Help** feature](#generate-a-help-command-for-you-package) for your package. 27 | - [Doc-tests](#doc-tests). 28 | 29 | 30 | The [guide][guide] fully describes the usage of this module and defines documentation syntax. 31 | 32 | ## Usage 33 | 34 | Using `tidy` is as simple as writing some doc-comments and calling: 35 | ```typ 36 | #import "@preview/tidy:0.4.3" 37 | 38 | #let docs = tidy.parse-module(read("my-module.typ")) 39 | #tidy.show-module(docs, style: tidy.styles.default) 40 | ``` 41 | 42 | The available predefined styles are currently `tidy.styles.default` and `tidy.styles.minimal`. Custom styles can be added by hand (take a look at the [user guide][guide]). 43 | 44 | ## Example 45 | 46 | A full example on how to use this module for your own package (maybe even consisting of multiple files) can be found at [examples](https://github.com/Mc-Zen/tidy/tree/main/examples). 47 | 48 | ```typ 49 | /// This function computes the cardinal sine, $sinc(x)=sin(x)/x$. 50 | /// 51 | /// ```example 52 | /// #sinc(0) 53 | /// ``` 54 | /// 55 | /// -> float 56 | #let sinc( 57 | /// The argument for the cardinal sine function. 58 | /// -> int | float 59 | x 60 | ) = if x == 0 {1} else {calc.sin(x) / x} 61 | ``` 62 | 63 | **tidy** turns this into: 64 | 65 |
66 | Tidy example output 67 |
68 | 69 | 70 | ## Access user-defined functions and images 71 | 72 | The code in the doc-comments is evaluated through the [`eval`](https://typst.app/docs/reference/foundations/eval/) function. In order to access user-defined functions and images, you can make use of the `scope` argument of `tidy.parse-module()`: 73 | 74 | ```typ 75 | #{ 76 | import "my-module.typ" 77 | let module = tidy.parse-module( 78 | read("my-module.typ"), 79 | scope: (my-module: my-module, img: an-image) 80 | ) 81 | let an-image = image("img.png") 82 | tidy.show-module( 83 | module, 84 | style: tidy.styles.default 85 | ) 86 | } 87 | ``` 88 | The doc-comments in `my-module.typ` may now access the image with `#img` and can call any function or variable from `my-module` in the style of `#my-module.my-function()`. This makes rendering examples right in the doc-comments as easy as a breeze! 89 | 90 | ## Generate a help command for you package 91 | With **tidy**, you can add a help command to you package that allows users to obtain the documentation of a specific definition or parameter right in the document. This is similar to CLI-style help commands. If you have already written doc-comments for your package, it is quite low-effort to add this feature. Once set up, the end-user can use it like this: 92 | 93 | ```typ 94 | // happily coding, but how do I use this one complex function again? 95 | 96 | #mypackage.help("func") 97 | #mypackage.help("func(param1)") // print only parameter description of param1 98 | ``` 99 | 100 | This will print the documentation of `func` directly into the document — no need to look it up in a manual. Read up on setup instructions in the [user guide][guide]. 101 | 102 | ## Doc-tests 103 | It is possible to add simple doc-tests — assertions that will be run when the documentation is generated. This is useful if you want to keep small tests and documentation in one place. 104 | ```typ 105 | /// #test( 106 | /// `num.my-square(2) == 4`, 107 | /// `num.my-square(4) == 16`, 108 | /// ) 109 | #let my-square(n) = n * n 110 | ``` 111 | 117 | A few test assertion functions are available to improve readability, simplicity, and error messages. Currently, these are `eq(a, b)` for equality tests, `ne(a, b)` for inequality tests and `approx(a, b, eps: 1e-10)` for floating point comparisons. These assertion helper functions are always available within doc-comment tests. 118 | 119 | 120 | ## Changelog 121 | 122 | ### v0.4.3 123 | _Automatic locale support_ 124 | - Tidy now detects the document language and adjusts words like "Parameters" and "Default" accordingly. It is still possible to set these values manually through the parameter `show-module.local-names`. 125 | - The word `Variables` can now also be customized. 126 | 127 | ### v0.4.2 128 | _Fixes and Improvements_ 129 | - Code examples can now also show code that is _not_ executed on lines starting with `<<<`. 130 | - The type `tiling` is now supported and it is shown with the new gradient. 131 | - Fixes formatting of multiline default arguments. 132 | 133 | ### v0.4.1 134 | _Fixes_ 135 | - Strings containing `"//"` can now be used in default arguments. 136 | - References like `@section` can now link to labels outside the documentation. 137 | - Fixes issues with upcoming Typst 0.13. 138 | 139 | ### v0.4.0 140 | _Major redesign of the documentation syntax_ 141 | - New features 142 | - New parser for the new documentation syntax. The old parser is still available and can be activated via `tidy.parse-module(old-syntax: true)`. There is a [migration guide][migration guide] for adopting the new syntax. 143 | - Cross-references to function arguments. 144 | - Support for detecting _curried functions_, i.e., function aliases with prepended arguments using the `.with()` function. 145 | 146 | 147 | ### v0.3.0 148 | _Adds a help feature and more options_ 149 | - New features: 150 | - Help feature. 151 | - `preamble` option for examples (e.g., to add `import` statements). 152 | - more options for `show-module`: `omit-private-definitions`, `omit-private-parameters`, `enable-cross-references`, `local-names` (for configuring language-specific strings). 153 | - Improvements: 154 | - Allow using `show-example()` as standalone. 155 | - Updated type names that changed with Typst 0.8.0, e.g., integer -> int. 156 | - Fixes: 157 | - allow examples with ratio widths if `scale-preview` is not `auto`. 158 | - `show-outline` 159 | - explicitly use `raw(lang: none)` for types and function names. 160 | 161 | ### v0.2.0 162 | - New features: 163 | - Add executable examples to doc-comments. 164 | - Documentation for variables (as well as functions). 165 | - Doc-tests. 166 | - Rainbow-colored types `color` and `gradient`. 167 | - Improvements: 168 | - Allow customization of cross-references through `show-reference()`. 169 | - Allow customization of spacing between functions through styles. 170 | - Allow color customization (especially for the `default` theme). 171 | - Fixes: 172 | - Empty parameter descriptions are omitted (if the corresponding option is set). 173 | - Trim newline characters from parameter descriptions. 174 | - ⚠️ Breaking changes: 175 | - Before, cross-references for functions using the `@@` syntax could omit the function parentheses. Now this is not possible anymore, since such references refer to variables now. 176 | - (only concerning custom styles) The style functions `show-outline()`, `show-parameter-list`, and `show-type()` now take `style-args` arguments as well. 177 | 178 | ### v0.1.0 179 | 180 | _Initial Release_ 181 | 182 | [guide]: https://github.com/Mc-Zen/tidy/releases/download/v0.4.3/tidy-guide.pdf 183 | 184 | [migration guide]: https://github.com/Mc-Zen/tidy/tree/v0.4.3/docs/migration-to-0.4.0.md -------------------------------------------------------------------------------- /src/helping.typ: -------------------------------------------------------------------------------- 1 | #import "styles.typ" 2 | #import "utilities.typ" 3 | #import "testing.typ" 4 | #import "parse-module.typ": parse-module 5 | #import "show-module.typ": show-module 6 | 7 | #let help-box(content) = { 8 | block( 9 | above: 1em, 10 | inset: 1em, 11 | stroke: rgb("#AAA"), 12 | fill: rgb("#F5F5F544"), { 13 | text(size: 1.8em, [? #smallcaps("help")#h(1fr)?]) 14 | text(.9em, content) 15 | } 16 | ) 17 | } 18 | 19 | #let parse-namespace-modules(entry, old-syntax: false) = { 20 | // "Module" is made up of several files 21 | if type(entry) != array { 22 | entry = (entry,) 23 | } 24 | parse-module(entry.map(x => x()).join("\n"), old-syntax: old-syntax, label-prefix: "help-") 25 | } 26 | 27 | #let search-docs(search, searching, namespace, style, old-syntax: false) = { 28 | if search == "" { return help-box(block[_empty search string_]) } 29 | let search-names = "n" in searching 30 | let search-descriptions = "d" in searching 31 | let search-parameters = "p" in searching 32 | 33 | let search-argument-dict(args) = { 34 | if search in args { return true } 35 | for (key, value) in args { 36 | if "description" in value and search in value.description { return true } 37 | } 38 | return false 39 | } 40 | 41 | let filter = definition => { 42 | (search-names and search in definition.name) or (search-descriptions and "description" in definition and search in definition.description) or (search-parameters and "args" in definition and search-argument-dict(definition.args)) 43 | } 44 | 45 | let definitions = () 46 | let module = parse-namespace-modules(namespace.at("."), old-syntax: old-syntax) 47 | let functions = () 48 | let variables = () 49 | for (name, modules) in namespace { 50 | let module = parse-namespace-modules(modules, old-syntax: old-syntax) 51 | 52 | functions += module.functions.filter(filter) 53 | variables += module.variables.filter(x => search in x.name or search in x.description) 54 | } 55 | module.functions = functions 56 | module.variables = variables 57 | return help-box({ 58 | show search: highlight.with(fill: rgb("#FF28")) 59 | show-module(module, style: style, enable-cross-references: false) 60 | }) 61 | } 62 | 63 | 64 | 65 | #let get-docs( 66 | definition-name, namespace, package-name, style, 67 | onerror: msg => assert(false, message: msg) 68 | ) = { 69 | let name = definition-name 70 | let result 71 | if type(name) == function { name = repr(name) } 72 | assert.eq(type(name), str, message: "The definition name has to be a string, found `" + repr(name) + "`") 73 | 74 | let name-components = name.split(".") 75 | name = name-components.pop() 76 | let module-name = name-components.join(".") 77 | 78 | if module-name == none { module-name = "." } 79 | 80 | if module-name not in namespace { 81 | return onerror("The package `" + package-name + "` contains no module `" + module-name + "`") 82 | } 83 | 84 | 85 | let module = parse-namespace-modules(namespace.at(module-name)) 86 | 87 | // We support selecting a specific parameter name (for functions) 88 | let param-name 89 | if "(" in name { 90 | let match = name.match(regex("(\w[\w\d\-_]*)\((.*)\)")) 91 | if match != none { 92 | (name, param-name) = match.captures 93 | if param-name == "" { param-name = none } 94 | definition-name = definition-name.slice(0, definition-name.position("(")) 95 | } 96 | } 97 | 98 | // First check if there is a function with the given name 99 | let definition-doc = module.functions.find(x => x.name == name) 100 | if definition-doc != none { 101 | if param-name != none { // extract only the parameter description 102 | let style-functions = utilities.get-style-functions(style) 103 | 104 | let style-args = ( 105 | style: style-functions, 106 | label-prefix: "", 107 | first-heading-level: 2, 108 | break-param-descriptions: true, 109 | omit-empty-param-descriptions: false, 110 | colors: styles.default.colors, 111 | enable-cross-references: false 112 | ) 113 | 114 | let eval-scope = ( 115 | // Predefined functions that may be called by the user in doc-comment code 116 | example: style-functions.show-example.with( 117 | inherited-scope: module.scope 118 | ), 119 | test: testing.test.with( 120 | inherited-scope: testing.assertations + module.scope, 121 | enable: false 122 | ), 123 | // Internally generated functions 124 | tidy: ( 125 | show-reference: style-functions.show-reference.with(style-args: style-args) 126 | ) 127 | ) 128 | 129 | eval-scope += module.scope 130 | 131 | style-args.scope = eval-scope 132 | 133 | 134 | // Show the docs 135 | if param-name not in definition-doc.args { 136 | if ".." + param-name in definition-doc.args { 137 | param-name = ".." + param-name 138 | } else { 139 | return onerror("The function `" + definition-name + "` has no parameter `" + param-name + "`") 140 | } 141 | } 142 | let info = definition-doc.args.at(param-name) 143 | let types = info.at("types", default: ()) 144 | let description = info.at("description", default: "") 145 | result = block(strong(name), above: 1.8em) 146 | result += (style.show-parameter-block)( 147 | param-name, types, utilities.eval-docstring(description, style-args), 148 | style-args, 149 | show-default: "default" in info, 150 | default: info.at("default", default: none), 151 | ) 152 | } 153 | module.functions = (definition-doc,) 154 | module.variables = () 155 | } else { 156 | let definition-doc = module.variables.find(x => x.name == name) 157 | if definition-doc != none { 158 | assert(param-name == none, message: "Parameters can only be specified for function definitions, not for variables. ") 159 | module.variables = (definition-doc,) 160 | module.functions = () 161 | } else { 162 | 163 | if module-name == "." { 164 | return onerror("The package `" + package-name + "` contains no (documented) definition `" + name + "`") 165 | } else { 166 | return onerror("The module `" + module-name + "` from the package `" + package-name + "` contains no (documented) definition `" + name + "`") 167 | } 168 | } 169 | } 170 | 171 | if result == none { 172 | result = show-module( 173 | module, 174 | style: style, 175 | enable-cross-references: false, 176 | enable-tests: false, 177 | show-outline: false, 178 | ) 179 | } 180 | return result 181 | } 182 | 183 | 184 | /// Generates a `help` function for your package that allows the user to 185 | /// prints references directly into their document while typing. This allows 186 | /// them to easily check the usage and documentation of a function or variable. 187 | #let generate-help( 188 | 189 | /// This dictionary should reflect the "namespace" of the package 190 | /// in a flat dictionary and contain `read.with()` instances for the respective code 191 | /// files. 192 | /// Imagine importing everything from a package, `#import "mypack.typ": *`. How a 193 | /// symbol is accessible now determines how the dictionary should be built. 194 | /// We start with a root key, `(".": read.with("lib.typ"))`. If `lib.typ` imports 195 | /// symbols from other files _into_ its scope, these files should be added to the 196 | /// root along with `lib.typ` by passing an array: 197 | /// ```typ 198 | /// ( 199 | /// ".": (read.with("lib.typ"), read.with("more.typ")), 200 | /// "testing": read.with("testing.typ") 201 | /// ) 202 | /// ``` 203 | /// Here, we already show another case: let `testing.typ` be imported in `lib.typ` 204 | /// but without `*`, so that the symbols are accessed via `testing.`. We therefore 205 | /// add these under a new key. Nested files should be added with multiple 206 | /// dots, e.g., `"testing.float."`. 207 | /// 208 | /// By providing instances of `read()` with the filename prepended, you allow tidy 209 | /// to read the files that are not part of the tidy package but at the same time 210 | /// enable lazy evaluation of the files, i.e., a file is only opened when a 211 | /// definition from this file is requested through `help()`. 212 | /// -> dictionary 213 | namespace: (".": () => ""), 214 | 215 | /// The name of the package. This is required to give helpful error messages when 216 | /// a symbol cannot be found. 217 | /// -> str 218 | package-name: "", 219 | 220 | /// A tidy style that is used for showing parts of the documentation 221 | /// in the help box. It is recommended to leave this at the `help` style which is 222 | /// particularly designed for this purpose. Please post an issue if you have problems 223 | /// or suggestions regarding this style. 224 | /// -> dictionary 225 | style: styles.help, 226 | 227 | /// What to do with errors. By default, an assertion is failed (the document panics). 228 | /// -> function 229 | onerror: msg => assert(false, message: msg), 230 | 231 | /// Whether to use the old parser. 232 | /// -> bool 233 | old-syntax: false 234 | ) = { 235 | 236 | let validate-namespace-tree(namespace) = { 237 | let validate-file-reader(file-reader) = { 238 | assert(type(file-reader) == function, message: "The namespace must have instances of `read.with([filename])` as leaves, found " + repr(file-reader)) 239 | } 240 | for (entry, value) in namespace { 241 | if type(value) == array { 242 | for file-reader in value { 243 | validate-file-reader(file-reader) 244 | } 245 | } else if type(value) == dictionary { 246 | validate-namespace-tree(value) 247 | } else { 248 | validate-file-reader(value) 249 | } 250 | } 251 | } 252 | 253 | 254 | validate-namespace-tree(namespace) 255 | 256 | 257 | let help-function = ( 258 | ..args, 259 | search: none, 260 | searching: "ndp", // Enable search of: name, descriptions, parameters 261 | style: style 262 | ) => { 263 | if search == none { 264 | if args.pos().len() == 0 { return none } 265 | let name = args.pos().first() 266 | help-box(get-docs(name, namespace, package-name, style, onerror: onerror)) 267 | } else { 268 | search-docs(search, searching, namespace, style, old-syntax: old-syntax) 269 | } 270 | } 271 | help-function 272 | } 273 | 274 | 275 | 276 | 277 | #let flatten-namespace(namespace) = { 278 | let sub-namespace-name = "" 279 | 280 | let flatten-impl(dict, name) = { 281 | let name-without-dot = name.trim(".") 282 | let flattened-dict = ((name-without-dot): ()) 283 | for (key, value) in dict { 284 | if type(value) == function { value = (value,) } 285 | if key == "." { 286 | flattened-dict.at(name-without-dot) += value 287 | } else if type(value) == array { 288 | flattened-dict.insert(name + key, value) 289 | } else if type(value) == dictionary { 290 | let u = flatten-impl(value, name + key + ".") 291 | flattened-dict += u 292 | } 293 | } 294 | return flattened-dict 295 | } 296 | let flattened-namespace = flatten-impl(namespace, "") 297 | 298 | } 299 | 300 | #flatten-namespace(( 301 | ".": read, 302 | "math": read, 303 | "matrix": ( 304 | ".": (read, read), 305 | "vector": ( 306 | "algebra": read, 307 | "addition": ( 308 | "binary": read 309 | ) 310 | ) 311 | ), 312 | 313 | )) -------------------------------------------------------------------------------- /src/new-parser.typ: -------------------------------------------------------------------------------- 1 | 2 | #let split-once(string, delimiter) ={ 3 | let pos = string.position(delimiter) 4 | if pos == none { return string } 5 | (string.slice(0, pos), string.slice(pos + 1)) 6 | } 7 | 8 | #let parse-argument-list(text) = { 9 | let brace-level = 1 10 | let literal-mode = none // Whether in ".." 11 | 12 | let args = () 13 | 14 | let arg = "" 15 | let is-named = false // Whether current argument is a named arg 16 | 17 | let previous-char = none // lookbehind of 1 18 | let count-processed-chars = 1 19 | 20 | let maybe-split-argument(arg, is-named) = { 21 | if is-named { 22 | return split-once(arg, ":").map(str.trim) 23 | } else { 24 | return (arg.trim(),) 25 | } 26 | } 27 | let skip-line = false 28 | 29 | for c in text { 30 | let ignore-char = false 31 | 32 | if c == "\"" and previous-char != "\\" { 33 | if literal-mode == none { literal-mode = "\"" } 34 | else if literal-mode == "\"" { literal-mode = none } 35 | } else if literal-mode == none { 36 | if c == "(" { brace-level += 1 } 37 | else if c == ")" { brace-level -= 1 } 38 | else if c == "," and brace-level == 1 { 39 | if is-named { 40 | let (name, value) = split-once(arg, ":").map(str.trim) 41 | args.push((name, value)) 42 | } else { 43 | arg = arg.trim() 44 | args.push((arg,)) 45 | } 46 | arg = "" 47 | ignore-char = true 48 | is-named = false 49 | } else if c == ":" and brace-level == 1 { 50 | is-named = true 51 | } else if c == "/" and previous-char == "/" { 52 | skip-line = true 53 | arg = arg.slice(0, -1) 54 | } else if c == "\n" { 55 | skip-line = false 56 | } 57 | } 58 | count-processed-chars += 1 59 | if brace-level == 0 { 60 | if arg.trim().len() > 0 { 61 | if is-named { 62 | let (name, value) = split-once(arg, ":").map(str.trim) 63 | args.push((name, value)) 64 | } else { 65 | arg = arg.trim() 66 | args.push((arg,)) 67 | } 68 | } 69 | break 70 | } 71 | if not (ignore-char or skip-line) { arg += c } 72 | previous-char = c 73 | } 74 | return ( 75 | args: args, 76 | brace-level: brace-level - 1, 77 | processed-chars: count-processed-chars - 1 78 | ) 79 | } 80 | 81 | // #assert.eq( 82 | // parse-argument-list("text)"), 83 | // (args: (("text",),), brace-level: -1, processed-chars: 5) 84 | // ) 85 | // #assert.eq( 86 | // parse-argument-list("pos,"), 87 | // (args: (("pos",),), brace-level: 0, processed-chars: 4) 88 | // ) 89 | // #assert.eq( 90 | // parse-argument-list("12, 13, a)"), 91 | // (args: (("12",), ("13",), ("a",)), brace-level: -1, processed-chars: 10) 92 | // ) 93 | // #assert.eq( 94 | // parse-argument-list("a: 2, b: 3)"), 95 | // (args: (("a", "2"), ("b", "3")), brace-level: -1, processed-chars: 11) 96 | // ) 97 | // #assert.eq( 98 | // parse-argument-list("a: 2 // 2\n)"), 99 | // (args: (("a", "2"),), brace-level: -1, processed-chars: 11) 100 | // ) 101 | // #assert.eq( 102 | // parse-argument-list("a: 2, // 2\nb)"), 103 | // (args: (("a", "2"),("b",)), brace-level: -1, processed-chars: 13) 104 | // ) 105 | 106 | 107 | #let eval-doc-comment-test((line-number, line), label-prefix: "") = { 108 | if line.starts-with(" >>> ") { 109 | return " #test(`" + line.slice(8) + "`, source-location: (module: \"" + parse-info.label-prefix + "\", line: " + str(line-number) + "))" 110 | } 111 | line 112 | } 113 | 114 | 115 | #let parse-description-and-types(lines, label-prefix: "", first-line-number: 0) = { 116 | 117 | 118 | let types = none 119 | if lines.last(default: "").contains("->") { 120 | let parts = lines.last().split("->") 121 | types = parts.last().split("|").map(str.trim) 122 | 123 | lines.last() = parts.slice(0, -1).join("->") 124 | } else { 125 | let line-index = lines 126 | .map(str.trim) 127 | .enumerate() 128 | .filter(((i, line)) => line.starts-with("->")) 129 | .map(((i, line)) => i) 130 | .first(default: none) 131 | 132 | if line-index != none { 133 | types = lines 134 | .slice(line-index) 135 | .join("\n") 136 | .trim() 137 | .trim("->") 138 | .split("|") 139 | .map(str.trim) 140 | lines = lines.slice(0, line-index) 141 | } 142 | } 143 | 144 | let description = lines 145 | // .enumerate(start: first-line-number) 146 | // .map(eval-doc-comment-test.with(label-prefix: label-prefix)) 147 | .join("\n") 148 | 149 | if description == none { description = "" } 150 | 151 | return ( 152 | description: description.trim(), 153 | types: types 154 | ) 155 | } 156 | 157 | #assert.eq( 158 | parse-description-and-types(("asd",)), 159 | (description: "asd", types: none) 160 | ) 161 | #assert.eq( 162 | parse-description-and-types(("->int",)), 163 | (description: "", types: ("int",)) 164 | ) 165 | #assert.eq( 166 | parse-description-and-types((" -> int",)), 167 | (description: "", types: ("int",)) 168 | ) 169 | #assert.eq( 170 | parse-description-and-types(("abcdefg -> int",)), 171 | (description: "abcdefg", types: ("int",)) 172 | ) 173 | #assert.eq( 174 | parse-description-and-types(("abcdefg", "-> int",)), 175 | (description: "abcdefg", types: ("int",)) 176 | ) 177 | 178 | 179 | 180 | #let trim-trailing-comments(line) = { 181 | let pos = line.position("//") 182 | if pos == none { return line } 183 | return line.slice(0, pos).trim() 184 | } 185 | 186 | #assert.eq(trim-trailing-comments("1+2+3+4 // 23"), "1+2+3+4") 187 | #assert.eq(trim-trailing-comments("1+2+3+4 // 23 // 3"), "1+2+3+4") 188 | 189 | 190 | 191 | 192 | #let definition-name-regex = regex(`#?let (\w[\w\d\-_]*)\s*(\(?)`.text) 193 | 194 | 195 | #let process-parameters(parameters) = { 196 | let processed-params = (:) 197 | 198 | for param in parameters { 199 | let param-parts = param.name 200 | let (description, types) = parse-description-and-types(param.desc-lines, label-prefix: "") 201 | let param-info = ( 202 | // name: param-parts.first(), 203 | description: description, 204 | ) 205 | if param-parts.len() == 2 { 206 | param-info.default = param-parts.last() 207 | } 208 | if types != none { 209 | param-info.types = types 210 | } 211 | processed-params.insert(param-parts.first(), param-info) 212 | } 213 | processed-params 214 | } 215 | 216 | 217 | 218 | #let process-definition(definition) = { 219 | let (description, types) = parse-description-and-types(definition.description, label-prefix: "") 220 | 221 | if definition.args == none { 222 | definition.remove("args") 223 | if types != none { 224 | definition.type = types.first() 225 | } 226 | } else { 227 | definition.return-types = types 228 | definition.args = process-parameters(definition.args) 229 | } 230 | definition.description = description 231 | definition 232 | } 233 | 234 | #let curry-matcher = regex(" *= *([.\w\d\-_]+)\.with\(") 235 | 236 | #let parameter-parser(state, line) = { 237 | if line.starts-with("///") { 238 | state.unmatched-description.push(line.slice(3)) 239 | } else { 240 | state.unfinished-param += line + "\n" 241 | 242 | let (args, brace-level, processed-chars) = parse-argument-list(state.unfinished-param) 243 | if brace-level == -1 { // parentheses are already closed on this line 244 | state.state = "finished" 245 | // let curry = state.unfinished-param.slice(processed-chars).match(curry-matcher) 246 | // if curry != none { 247 | // state.curry = (name: curry.captures.first(), rest: state.unfinished-param.slice(processed-chars + curry.end)) 248 | // } 249 | } 250 | if args.len() > 0 and (state.unfinished-param.trim("\n").ends-with(",") or state.state == "finished") { 251 | state.params.push((name: args.first(), desc-lines: state.unmatched-description)) 252 | state.unmatched-description = () 253 | state.params += args.slice(1).map(arg => (name: arg, desc-lines: ())) 254 | state.unfinished-param = "" 255 | } 256 | } 257 | return state 258 | } 259 | 260 | #let process-curry-info(info) = { 261 | let pos = info.args 262 | .filter(x => x.name.len() == 1) 263 | .map(x => x.name.at(0)) 264 | let named = info.args 265 | .filter(x => x.name.len() == 2) 266 | .map(x => x.name).to-dict() 267 | 268 | ( 269 | name: info.name, 270 | pos: pos, 271 | named: named 272 | ) 273 | } 274 | 275 | 276 | #let parse(src) = { 277 | let lines = (src.split("\n") + ("",)).map(line => { 278 | // return line.trim() 279 | // trim only doc-comment lines 280 | let l = line.trim(at: start) 281 | if l.starts-with("///") { l } 282 | else { line.trim(at: end) } 283 | }) 284 | 285 | let module-description = none 286 | let definitions = () 287 | 288 | 289 | // Parser state 290 | let name = none 291 | let found-code = false // are we still looking for a potential module description? 292 | let args = () 293 | let desc-lines = () 294 | let curry-info = none 295 | 296 | 297 | let param-parser-default = ( 298 | state: "idle", 299 | params: (), 300 | unmatched-description: (), 301 | unfinished-param: "" 302 | ) 303 | let param-parser = param-parser-default 304 | let finished-definition = false 305 | 306 | for line in lines { 307 | if param-parser.state == "finished" { 308 | let curry = param-parser.at("curry", default: none) 309 | 310 | if curry-info != none { 311 | finished-definition = true 312 | curry-info.args = param-parser.params 313 | param-parser = param-parser-default 314 | args = () 315 | } else { 316 | args = param-parser.params 317 | if "curry" in param-parser { 318 | // let curry = param-parser.curry 319 | // curry-info = (name: curry.name) 320 | // param-parser = param-parser-default 321 | // param-parser.state = "running" 322 | // param-parser = parameter-parser(param-parser, curry.rest) 323 | // if param-parser.state == "finished" { 324 | // finished-definition = true 325 | // param-parser = param-parser-default 326 | // } 327 | } else { 328 | finished-definition = true 329 | param-parser = param-parser-default 330 | } 331 | } 332 | } 333 | if param-parser.state == "running" { 334 | param-parser = parameter-parser(param-parser, line) 335 | if param-parser.state == "running" { continue } 336 | } 337 | 338 | if finished-definition { 339 | if name != none { 340 | definitions.push((name: name, description: desc-lines, args: args)) 341 | if curry-info != none { 342 | definitions.at(-1).parent = process-curry-info(curry-info) 343 | curry-info = none 344 | } 345 | } 346 | desc-lines = () 347 | name = none 348 | finished-definition = false 349 | } 350 | 351 | 352 | if line.starts-with("///") { // is a doc-comment line 353 | desc-lines.push(line.slice(3)) 354 | } else if desc-lines != () { 355 | // look for something to attach the doc-comment to 356 | // (a parameter or a definition) 357 | 358 | line = line.trim("#", at: start) 359 | if line.starts-with("let ") and name == none { 360 | 361 | found-code = true 362 | let match = line.match(definition-name-regex) 363 | if match != none { 364 | name = match.captures.first() 365 | if match.captures.at(1) != "" { // it's a function 366 | param-parser.state = "running" 367 | param-parser = parameter-parser(param-parser, line.slice(match.end)) 368 | } else { // it's a variable or a function alias 369 | args = none 370 | finished-definition = true 371 | let p = line.slice(match.end) 372 | 373 | let curry = line.slice(match.end).match(curry-matcher) 374 | if curry != none { 375 | curry-info = (name: curry.captures.first()) 376 | param-parser = parameter-parser(param-parser, line.slice(match.end + curry.end)) 377 | // param-parser.curry = (name: curry.captures.first(), rest: state.unfinished-param.slice(processed-chars + curry.end)) 378 | } 379 | } 380 | } 381 | 382 | } else { // neither /// nor (#)let 383 | if not found-code { 384 | found-code = true 385 | module-description = desc-lines.join("\n") 386 | } 387 | if name == none { 388 | desc-lines = () 389 | } 390 | 391 | } 392 | } 393 | } 394 | 395 | definitions = definitions.map(process-definition) 396 | ( 397 | description: module-description, 398 | functions: definitions.filter(x => "args" in x), 399 | variables: definitions.filter(x => "args" not in x), 400 | ) 401 | } 402 | 403 | -------------------------------------------------------------------------------- /src/old-parser.typ: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Matches Typst doc-comment for a function declaration. Example: 4 | // 5 | // // This function does something 6 | // // 7 | // // param1 (str): This is param1 8 | // // param2 (content, length): This is param2. 9 | // // Yes, it really is. 10 | // #let something(param1, param2) = { 11 | // 12 | // } 13 | // 14 | // The entire block may be indented by any amount, the declaration can either start with `#let` or `let`. The docstring must start with `///` on every line and the function declaration needs to start exactly at the next line. 15 | // #let docstring-matcher = regex(`((?:[^\S\r\n]*/{3} ?.*\n)+)[^\S\r\n]*#?let (\w[\w\d\-_]+)`.text) 16 | // #let docstring-matcher = regex(`([^\S\r\n]*///.*(?:\n[^\S\r\n]*///.*)*)\n[^\S\r\n]*#?let (\w[\w\d\-_]*)`.text) 17 | #let docstring-matcher = regex(`(?m)^((?:[^\S\r\n]*///.*\n)+)[^\S\r\n]*#?let (\w[\w\d\-_]*)`.text) 18 | // The regex explained: 19 | // 20 | // First capture group: ([^\S\r\n]*///.*(?:\n[^\S\r\n]*///.*)*) 21 | // is for the docstring. It may start with any whitespace [^\S\r\n]* 22 | // and needs to have /// followed by anything. This is the first line of 23 | // the docstring and we treat it separately only in order to be able to 24 | // match the very first line in the file (which is otherwise tricky here). 25 | // We then match basically the same thing n times: \n[^\S\r\n]*///.*)* 26 | // 27 | // We then want a linebreak (should also have \r here?), arbitrary whitespace 28 | // and the word let or #let: \n[^\S\r\n]*#?let 29 | // 30 | // Second capture group: (\w[\w\d\-_]*) 31 | // Matches the function name (any Typst identifier) 32 | 33 | 34 | // Matches an argument documentation of the form `/// - myparameter (str)`. 35 | #let argument-documentation-matcher = regex(`[^\S\r\n]*/{3} - ([.\w\d\-_]+) \(([\w\d\-_ ,]+)\): ?(.*)`.text) 36 | 37 | 38 | 39 | #let split-once(string, delimiter) ={ 40 | let pos = string.position(delimiter) 41 | if pos == none { return string } 42 | (string.slice(0, pos), string.slice(pos + 1)) 43 | } 44 | 45 | /// #set raw(lang: "typc") 46 | /// Parse a Typst argument list either at 47 | /// - call site, e.g., `f("Timbuktu", value: 23)` or at 48 | /// - declaration, e.g. `let f(place, value: 0)`. 49 | /// 50 | /// This function returns a dictionary `(pos, named, count-processed-chars)` where 51 | /// `count-processed-chars` is the number of processed characters, i.e., the 52 | /// length of the argument list and `pos` and `named` contain the arguments. 53 | /// 54 | /// 55 | /// This function returns `none`, if the argument list is not properly closed. 56 | /// Note, that valid Typst code is expected. 57 | /// 58 | /// *Example: * Calling this function with the following string 59 | /// 60 | /// ``` 61 | /// "#let func(p1, p2: 3pt, p3: (), p4: (entries: ())) = {...}" 62 | /// ``` 63 | /// 64 | /// and index `9` (which points to the opening parenthesis) yields the result 65 | /// ``` 66 | /// ( 67 | /// pos: ("p1", "p5"), 68 | /// named: ( 69 | /// p2: "3pt", 70 | /// p3: "()", 71 | /// p4: "(entries: ())" 72 | /// ) 73 | /// 44, 74 | /// ) 75 | /// ``` 76 | /// 77 | /// This function can deal with 78 | /// - any number of opening and closing parenthesis 79 | /// - string literals 80 | /// We don't deal with: 81 | /// - commented out code (`//` or `/**/`) 82 | /// - raw strings with #raw("``") syntax that contain `"` or `(` or `)` 83 | /// 84 | /// - text (str): String to parse. 85 | /// - index (int): Position of the opening parenthesis of the argument list. 86 | /// -> dictionary 87 | #let parse-argument-list(text, index) = { 88 | if text.len() <= index or text.at(index) != "(" { return none } 89 | if text.len() <= index or text.at(index) != "(" { return ((:), 0) } 90 | index += 1 91 | let brace-level = 1 92 | let literal-mode = none // Whether in ".." 93 | 94 | let positional = () 95 | let named = (:) 96 | let sink 97 | 98 | let arg = "" 99 | let is-named = false // Whether current argument is a named arg 100 | 101 | let previous-char = none 102 | let count-processed-chars = 1 103 | 104 | let maybe-split-argument(arg, is-named) = { 105 | if is-named { 106 | return split-once(arg, ":").map(str.trim) 107 | } else { 108 | return (arg.trim(),) 109 | } 110 | } 111 | 112 | for c in text.slice(index) { 113 | let ignore-char = false 114 | if c == "\"" and previous-char != "\\" { 115 | if literal-mode == none { literal-mode = "\"" } 116 | else if literal-mode == "\"" { literal-mode = none } 117 | } 118 | if literal-mode == none { 119 | if c == "(" { brace-level += 1 } 120 | else if c == ")" { brace-level -= 1 } 121 | else if c == "," and brace-level == 1 { 122 | if is-named { 123 | let (name, value) = split-once(arg, ":").map(str.trim) 124 | named.insert(name, value) 125 | } else { 126 | arg = arg.trim() 127 | if arg.starts-with("..") { sink = arg } 128 | else { positional.push(arg) } 129 | } 130 | arg = "" 131 | ignore-char = true 132 | is-named = false 133 | } else if c == ":" and brace-level == 1 { 134 | is-named = true 135 | } 136 | } 137 | count-processed-chars += 1 138 | if brace-level == 0 { 139 | if arg.trim().len() > 0 { 140 | if is-named { 141 | let (name, value) = split-once(arg, ":").map(str.trim) 142 | named.insert(name, value) 143 | } else { 144 | arg = arg.trim() 145 | if arg.starts-with("..") { sink = arg } 146 | else { positional.push(arg) } 147 | } 148 | } 149 | break 150 | } 151 | if not ignore-char { arg += c } 152 | previous-char = c 153 | } 154 | if brace-level > 0 { return none } 155 | return ( 156 | pos: positional, 157 | named: named, 158 | sink: sink, 159 | count: count-processed-chars 160 | ) 161 | } 162 | 163 | /// This is similar to @@parse-argument-list but focuses on parameter lists 164 | /// at the declaration site. 165 | /// 166 | /// If the argument list is well-formed, a dictionary is returned with 167 | /// an entry for each parsed 168 | /// argument name. The values are dictionaries that may be empty or 169 | /// have an entry for the key `default` containing a string with the parsed 170 | /// default value for this argument. 171 | /// 172 | /// 173 | /// 174 | /// *Example* \ 175 | /// Let us take the string 176 | /// ```typc 177 | /// "#let func(p1, p2: 3pt, p3: (), p4: (entries: ())) = {...}" 178 | /// ``` 179 | /// Here, we would call `parse-parameter-list(source-code, 9)` and retrieve 180 | /// #pad(x: 1em, ```typc 181 | /// ( 182 | /// p0: (:), 183 | /// p1: (default: "3pt"), 184 | /// p2: (default: "()"), 185 | /// p4: (default: "(entries: ())"), 186 | /// ) 187 | /// ```) 188 | /// 189 | /// - text (str): String to parse. 190 | /// - index (int): Index where the argument list starts. This index should 191 | /// point to the character *next* to the function name, i.e., to the 192 | /// opening brace `(` of the argument list if there is one (note, that 193 | /// function aliases for example produced by `myfunc.where(arg1: 3)` do 194 | /// not have an argument list). 195 | /// -> none, dictionary 196 | #let parse-parameter-list(text, index) = { 197 | let result = parse-argument-list(text, index) 198 | if result == none { return none } 199 | let (pos, named, count) = result 200 | let args = (:) 201 | for arg in arg-strings { 202 | if arg.len() == 1 { 203 | args.insert(arg.at(0), (:)) 204 | } else { 205 | args.insert(arg.at(0), (default: arg.at(1))) 206 | } 207 | } 208 | return (args: args, count: count) 209 | } 210 | 211 | 212 | // Take the result of `parse-argument-list()` and retrieve a list of positional 213 | // and named arguments, respectively. The values are `eval()`ed. 214 | // #let parse-arg-strings(args) = { 215 | // let positional-args = () 216 | // let named-args = (:) 217 | // for arg in args { 218 | // if arg.len() == 1 { 219 | // positional-args.push(eval(arg.at(0))) 220 | // } else { 221 | // named-args.insert(arg.at(0), eval(arg.at(1))) 222 | // } 223 | // } 224 | // return (pos: positional-args, named: named-args) 225 | // } 226 | 227 | 228 | 229 | /// Count the occurences of a single character in a string 230 | /// 231 | /// - string (str): String to investigate. 232 | /// - char (str): Character to count. The string needs to be of length 1. 233 | /// - start (int): Start index. 234 | /// - end (end): Start index. If `-1`, the entire string is searched. 235 | /// -> int 236 | #let count-occurences(string, char, start: 0, end: -1) = { 237 | let count = 0 238 | if end == -1 { end = string.len() } 239 | for c in string.slice(start, end) { 240 | if c == char { count += 1 } 241 | } 242 | // let i = 0 243 | // while i < end { 244 | // if string.at(i) == char { count += 1} 245 | // i += 1 246 | // } 247 | count 248 | } 249 | 250 | #let parse-description-and-documented-args(docstring, parse-info, first-line-number: 0) = { 251 | 252 | let fn-desc = "" 253 | let started-args = false 254 | let documented-args = () 255 | let return-types = none 256 | 257 | for (line-number, line) in docstring.split("\n").enumerate(start: first-line-number) { 258 | // Check if line is a test line -> replace it with a call to #test() 259 | if line.starts-with("/// >>> ") { 260 | line = "/// #test(`" + line.slice(8) + "`, source-location: (module: \"" 261 | line += parse-info.label-prefix + "\", line: " + str(line-number) + "))" 262 | } 263 | let arg-match = line.match(argument-documentation-matcher) 264 | if arg-match == none { 265 | let trimmed-line = line.trim().trim("/") 266 | if trimmed-line.trim().starts-with("->") { 267 | return-types = trimmed-line.trim().slice(2).split(",").map(x => x.trim()) 268 | } else { 269 | if not started-args { fn-desc += trimmed-line + "\n"} 270 | else { 271 | documented-args.last().desc += "\n" + trimmed-line 272 | } 273 | } 274 | } else { 275 | started-args = true 276 | let param-name = arg-match.captures.at(0) 277 | let param-types = arg-match.captures.at(1).split(",").map(x => x.trim()) 278 | let param-desc = arg-match.captures.at(2) 279 | documented-args.push((name: param-name, types: param-types, desc: param-desc)) 280 | } 281 | } 282 | return ( 283 | description: fn-desc, 284 | args: documented-args, 285 | return-types: return-types 286 | ) 287 | } 288 | 289 | #let parse-variable-docstring(source-code, match, parse-info) = { 290 | let docstring = match.captures.at(0) 291 | let name = match.captures.at(1) 292 | 293 | let first-line-number = count-occurences(source-code, "\n", end: match.start) + 1 294 | 295 | let (description, return-types) = parse-description-and-documented-args(docstring, parse-info, first-line-number: first-line-number) 296 | 297 | let var-specs = ( 298 | name: name, 299 | description: description, 300 | ) 301 | if return-types != none and return-types.len() > 0 { 302 | var-specs.type = return-types.first() 303 | } 304 | return var-specs 305 | } 306 | 307 | #let curry-matcher = regex(" *= *([.\w\d\-_]+)\.with\(") 308 | 309 | #let parse-curried-function(source-code, index) = { 310 | // let docstring = match.captures.at(0) 311 | // let var-name = match.captures.at(1) 312 | let line-end = source-code.slice(index).position("\n") 313 | let k = (line-end, source-code.slice(index)) 314 | if line-end == none { line-end = source-code.len() } 315 | else {line-end += index } 316 | let rest = source-code.slice(index, line-end) 317 | 318 | let match = rest.match(curry-matcher) 319 | if match == none { return none } 320 | 321 | let (pos, named, count) = parse-argument-list(source-code, match.end + index - 1) 322 | return ( 323 | name: match.captures.first(), 324 | pos: pos, 325 | named: named 326 | ) 327 | } 328 | 329 | /// Parse a function doc-comment that has been located in the source code with 330 | /// given match. 331 | /// 332 | /// The return value is a dictionary with the keys 333 | /// - `name` (str): the function name. 334 | /// - `description` (content): the function description. 335 | /// - `args`: A dictionary containing the argument list. 336 | /// - `return-types` (array(str)): A list of possible return types. 337 | /// 338 | /// The entries of the argument list dictionary are 339 | /// - `default` (str): the default value for the argument. 340 | /// - `description` (content): the argument description. 341 | /// - `types` (array(str)): A list of possible argument types. 342 | /// Every entry is optional and the dictionary also contains any non-documented 343 | /// arguments. 344 | /// 345 | /// 346 | /// 347 | /// - source-code (str): The source code containing some documented Typst code. 348 | /// - match (match): A regex match that matches a documentation string. The first 349 | /// capture group should hold the entire, raw docstring and the second capture 350 | /// the function name (excluding the opening parenthesis of the argument list 351 | /// if present). 352 | /// - parse-info (dictionary): 353 | /// -> dictionary 354 | #let parse-function-docstring(source-code, match, parse-info) = { 355 | let docstring = match.captures.at(0) 356 | let fn-name = match.captures.at(1) 357 | 358 | let first-line-number = count-occurences(source-code, "\n", end: match.start) + 1 359 | 360 | let (description, args: documented-args, return-types) = parse-description-and-documented-args(docstring, parse-info, first-line-number: first-line-number) 361 | 362 | 363 | // let (args, count) = parse-parameter-list(source-code, match.end) 364 | let (pos, named, sink, count) = parse-argument-list(source-code, match.end) 365 | let args = (:) 366 | for arg in pos { args.insert(arg, (:)) } 367 | for (arg, value) in named { args.insert(arg, (default: value)) } 368 | if sink != none { args.insert(sink, (:)) } 369 | 370 | 371 | for arg in documented-args { 372 | if arg.name in args { 373 | args.at(arg.name).description = arg.desc.trim("\n") 374 | args.at(arg.name).types = arg.types 375 | } else { 376 | assert( 377 | false, 378 | message: "The parameter `" + arg.name + "` does not appear in the argument list of the function `" + fn-name + "`" 379 | ) 380 | } 381 | } 382 | if parse-info.require-all-parameters { 383 | for arg in args { 384 | assert( 385 | documented-args.find(x => x.name == arg.at(0)) != none, 386 | message: "The parameter `" + arg.at(0) + "` of the function `" + fn-name + "` is not documented. " 387 | ) 388 | } 389 | } 390 | return ( 391 | name: fn-name, 392 | description: description, 393 | args: args, 394 | return-types: return-types 395 | ) 396 | } 397 | 398 | 399 | #let module-docstring-matcher = regex(`(?m)^((?:[^\S\r\n]*///.*\n)+)\n`.text) 400 | 401 | #let parse-module-docstring(source-code, parse-info) = { 402 | let match = source-code.match(module-docstring-matcher) 403 | if match == none { return none } 404 | let desc = parse-description-and-documented-args(match.captures.first(), parse-info, first-line-number: 0) 405 | return desc.description.trim() 406 | } -------------------------------------------------------------------------------- /docs/tidy-guide.typ: -------------------------------------------------------------------------------- 1 | #import "template.typ": * 2 | #import "/src/tidy.typ" 3 | 4 | #let version = toml("/typst.toml").package.version 5 | #show "tidy:0.0.0": "tidy:" + version 6 | 7 | 8 | #show: project.with( 9 | title: "Tidy", 10 | subtitle: "Keep your code tidy.", 11 | authors: ( 12 | "Mc-Zen", 13 | ), 14 | abstract: [ 15 | *tidy* is a package that generates documentation directly in #link("https://typst.app/", [Typst]) for your Typst modules. It parses doc-comments and can be used to easily build a reference section for each module. 16 | ], 17 | date: datetime.today().display("[month repr:long] [day], [year]"), 18 | version: version, 19 | url: "https://github.com/Mc-Zen/tidy" 20 | ) 21 | 22 | 23 | 24 | = Introduction 25 | 26 | You can easily feed *tidy* your in-code documented source files and get beautiful documentation of all your functions and variables printed out. 27 | The main features are: 28 | - Type annotations, 29 | - seamless cross references, 30 | - rendering code examples (see @preview-examples), 31 | - help command generation (see @help-command), and 32 | - doc-comment testing (see @doc-comment-testing). 33 | First, we import *tidy*. 34 | ```typ 35 | #import "@preview/tidy:0.0.0" 36 | ``` 37 | 38 | We now assume we have a Typst module called `repeater.typ`, containing a definition for a function named `repeat()`. 39 | 40 | 41 | #let example-code = read("/examples/repeater.typ") 42 | #file-code("repeater.typ", raw(block: true, lang: "typ", example-code)) 43 | 44 | Tidy uses `///` doc-comments for documentation. 45 | A function or variable can be provided with a *description* by placing a doc-comment just before the definition. 46 | 47 | Until type annotations are natively available in Typst, a return type can be annotated with the `->` syntax in the last line of the description. If there is more than one possible return type, the types can be given separated by the pipe `|` operator, e.g., `-> int | float`. 48 | 49 | Function arguments are documented in the same way. 50 | All descriptions are parsed as Typst markup. Take a look at @user-defined-symbols on how to add images or examples to a description. 51 | 52 | 53 | Calling #ref-fn("parse-module()") will read out the documentation of the given string (for example loaded from a file). We can then invoke #ref-fn("show-module()") on the returned docs object. The actual output depends on the utilized style template, see @customizing. 54 | 55 | ```typ 56 | #let docs = tidy.parse-module(read("docs.typ"), name: "Repeater") 57 | #tidy.show-module(docs) 58 | ``` 59 | 60 | This will produce the following output. 61 | #tidy-output-figure( 62 | tidy.show-module( 63 | tidy.parse-module(example-code, name: "Repeater", old-syntax: false), 64 | style: tidy.styles.default, 65 | first-heading-level: 3 66 | ) 67 | ) 68 | 69 | 70 | Cool, he? 71 | 72 | By default, an outline for all definitions is displayed at the top. This behaviour can be turned off with the parameter `show-outline` of #ref-fn("show-module()"). 73 | 74 | There is another nice little feature: in the doc-comment, you can cross-reference other definitions with the standard Typst syntax for referencing objects, e.g., `@repeat` or `@awful-pi`. This will automatically create a link that when clicked in the PDF will lead you to the documentation of that definition. Parameters of functions can be referenced as `@repeat.num`. 75 | 76 | 77 | Of course, compilation happens almost instantaneously, so you can see the live result while writing the docs for your package. Keep your code documented! 78 | 79 | 80 | = More options 81 | 82 | Sometimes you might want to document "private" functions and variables but omit them in the public documentation. In order to hide all definitions starting with an underscore, you may set `omit-private-definitions` to `true` in the call to #ref-fn("show-module()"). Similarly, "internal" parameters of otherwise public functions can be concealed by naming them with a leading underscore and setting `omit-private-parameters` to `true` as well. 83 | 84 | 85 | = Accessing user-defined symbols 86 | 87 | 88 | This package uses the Typst function #raw(lang: "typc", "eval()") to process function and parameter descriptions in order to enable arbitrary Typst markup within those. Since #raw(lang: "typc", "eval()") does not allow access to the filesystem and evaluates the content in a context where no user-defined variables or functions are available, it is not possible to directly call #raw(lang: "typ", "#import"), #raw(lang: "typ", "#image") or functions that you define in your code. 89 | 90 | Nevertheless, definitions can be made accessible with *tidy* by passing them to #ref-fn("parse-module()") through the optional `scope` parameter in form of a dictionary: 91 | ```typ 92 | #let make-square(width) = rect(width: width, height: width) 93 | #tidy.parse-module( 94 | read("my-module.typ"), scope: (make-square: make-square) 95 | ) 96 | ``` 97 | This makes any symbol in specified in the `scope` dictionary available under the name of the key. A function declared in `my-module.typ` can now use this variable in the description: 98 | ```typ 99 | /// This is a function 100 | /// #make-square(20pt) 101 | #let my-function() = {} 102 | ``` 103 | 104 | It is even possible to add *entire modules* to the scope which makes rendering examples using your module really easy. Let us say the file `wiggly.typ` contains: 105 | 106 | #file-code("wiggly.typ", raw(lang: "typ", block: true, read("/examples/wiggly.typ"))) 107 | 108 | 109 | Note, that we use the predefined `example` language here to show the code as well as the rendered output of some demo usage of our function. Options for previewing code examples are treated more in-detail in @preview-examples. 110 | 111 | We can now parse the module and make the module `wiggly` available through the `scope` parameter. Furthermore, we apply another trick: by specifying a `preamble`, we can add code to run before each example. Here we use this feature to import everything from the module `wiggly`. This way, we can directly write `draw-sine(...)` in the example (instead of `wiggly.draw-sine(...)`): 112 | ```typ 113 | #import "wiggly.typ" // don't import something specific from the module! 114 | 115 | #let docs = tidy.parse-module( 116 | read("wiggly.typ"), 117 | name: "wiggly", 118 | scope: (wiggly: wiggly), 119 | preamble: "#import wiggly: *\n" 120 | ) 121 | ``` 122 | 123 | In the output, the preview of the code examples is shown next to it. 124 | 125 | #{ 126 | import "/examples/wiggly.typ" 127 | 128 | let module = tidy.parse-module( 129 | read("/examples/wiggly.typ"), 130 | name: "wiggly", 131 | scope: (wiggly: wiggly), 132 | label-prefix: "wiggly1-", 133 | preamble: "#import wiggly: *\n", 134 | old-syntax: false 135 | ) 136 | tidy-output-figure(tidy.show-module( 137 | module, 138 | show-outline: false, 139 | break-param-descriptions: true, 140 | first-heading-level: 3 141 | ), breakable: true) 142 | } 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | = Preview examples 153 | 154 | As we already saw in the previous section, a function, variable, or parameter description can contain code examples that are automatically rendered and displayed side-by-side with the code. 155 | 156 | For this purpose the two #raw(lang: "typc", "raw") languages `example` (for Typst markup mode) 157 | ````typ 158 | /// ```example 159 | /// #sinc(0) 160 | /// ``` 161 | ```` 162 | and `examplec` (for Typst code mode) 163 | ````typ 164 | /// ```examplec 165 | /// sinc(0) 166 | /// ``` 167 | ```` 168 | are available in all doc-comments. 169 | In both versions, you can insert _hidden_ code lines starting with `>>>` anywhere in the demo code. These lines will just be executed but not displayed. 170 | ````typ 171 | /// ```examplec 172 | /// >>> import my-math: sinc // just executed, not shown 173 | /// sinc(0) 174 | /// ``` 175 | ```` 176 | This is useful for many scenarios like import statements, wrapping everything inside a container of a fixed size and other things. 177 | 178 | Conversely, lines starting with `<<<` will only be shown but not executed. 179 | 180 | #pagebreak() 181 | 182 | As an alternative, the function `example()` provides some bells and whistles which are showcased with the following `example-demo.typ` module which contains a function for highlighting text with gradients #footnote[which seems not very advisable due to the poor readability.]: 183 | 184 | 185 | #{ 186 | set text(size: .89em) 187 | import "/examples/example-demo.typ" 188 | 189 | let module = tidy.parse-module( 190 | read("/examples/example-demo.typ"), 191 | scope: (example-demo: example-demo), 192 | old-syntax: false 193 | ) 194 | tidy-output-figure(tidy.show-module(module, show-outline: false, break-param-descriptions: true)) 195 | } 196 | 197 | #pagebreak() 198 | 199 | == Standalone usage of example previews 200 | 201 | The example preview feature can also be used to add self-compiling code examples independently of *tidy*. For this, *tidy* provides the function #ref-fn("render-examples()"). 202 | ````typ 203 | #import "@preview/tidy:0.0.0": render-examples 204 | #show: render-examples 205 | 206 | ```example 207 | # 208 | ``` 209 | ```` 210 | 211 | 212 | It also features a `scope` argument, that can be pre-set: 213 | 214 | ````typ 215 | #show: render-examples.with(scope: (answer: 42)) 216 | 217 | ```example 218 | #answer 219 | ``` 220 | ```` 221 | 222 | Furthermore, the output format of the example can be customized through the parameter `layout` of `render-examples`. This parameter takes a `function` with two positional arguments: the #raw(lang: "typc", "raw") element and the preview. 223 | ````typ 224 | #show: render-examples.with( 225 | layout: (code, preview) => grid(code, preview) 226 | ) 227 | ```` 228 | 229 | 230 | #pagebreak() 231 | 232 | 233 | = Customizing the style 234 | 235 | There are multiple ways to customize the output style. You can 236 | - pick a different predefined style template, 237 | - apply show rules before printing the module documentation or 238 | - create an entirely new style template. 239 | 240 | 241 | A different predefined style template can be selected by passing a style to the `style` parameter: 242 | ```typ 243 | #tidy.show-module( 244 | tidy.parse-module(read("my-module.typ")), 245 | style: tidy.styles.minimal, 246 | ) 247 | ``` 248 | 249 | You can use show rules to customize the document style template before calling #ref-fn("show-module()"). Setting any text and paragraph attributes works just out of the box. Furthermore, heading styles can be set to affect the appearance of the module name (relative heading level 1), function or variable names (relative heading level 2) and the word *Parameters* (relative heading level 3), all relative to what is set with the parameter `first-heading-level` of #ref-fn("show-module()"). 250 | 251 | Finally, if that is not enough, you can design a completely new style template. Examples thereof can be found in the folder `src/styles/` in the #link("https://github.com/Mc-Zen/tidy", "GitHub Repository"). 252 | 253 | 254 | == Customizing Colors (mainly for the `default` style) 255 | 256 | The colors used by a style (especially the color in which types are shown) can be set through the option `colors` of #ref-fn("show-module()"). It expects a dictionary with colors as values. Possible keys are all type names as well as `signature-func-name` which sets the color of the function name as shown in a function signature. 257 | 258 | The `default` theme defines a color scheme `colors-dark` along with the default `colors` which adjusts the plain colors for better readability on a dark background. 259 | 260 | ```typ 261 | #tidy.show-module( 262 | docs, 263 | colors: tidy.styles.default.colors-dark 264 | ) 265 | ``` 266 | With a dark background and light text, these colors produce much better contrast than the default colors: 267 | #{ 268 | set text(fill: luma(240)) 269 | 270 | let module = tidy.parse-module( 271 | ``` 272 | /// Produces space. 273 | #let space( 274 | /// -> length 275 | amount 276 | ) 277 | ```.text, 278 | old-syntax: false 279 | ) 280 | tidy-output-figure( 281 | tidy.show-module( 282 | module, 283 | show-outline: false, 284 | colors: tidy.styles.default.colors-dark, 285 | style: tidy.styles.default, 286 | first-heading-level: 3 287 | ), 288 | fill: luma(20) 289 | ) 290 | } 291 | 292 | #pagebreak() 293 | 294 | == Predefined styles 295 | Currently, the two predefined styles `tidy.styles.default` and `tidy-styles.minimal` are available. 296 | - `tidy.styles.default`: Loosely imitates the online documentation of Typst functions. 297 | - `tidy.styles.minimal`: A very light and space-efficient theme that is oriented around simplicity. With this theme, the example from above looks like the following: 298 | #{ 299 | import "/examples/wiggly.typ" 300 | 301 | let module = tidy.parse-module( 302 | read("/examples/wiggly.typ"), 303 | name: "wiggly", 304 | scope: (wiggly: wiggly), 305 | label-prefix: "wiggly2-", 306 | preamble: "#import wiggly: *\n", 307 | old-syntax: false 308 | ) 309 | tidy-output-figure( 310 | tidy.show-module( 311 | module, 312 | show-outline: false, 313 | style: tidy.styles.minimal, 314 | first-heading-level: 3 315 | ) 316 | ) 317 | } 318 | 319 | 320 | 321 | 322 | #pagebreak() 323 | = Help command 324 | 325 | #text(red)[This feature is still experimental and may change a bit in its details. Output customization will be made available with the introduction of user-defined types into Typst. The _search_ feature will then move into a nested function, i.e., `help.search()`. ] 326 | 327 | With *tidy*, you can easily add a `help` command to your package. This allows the users of your package to call #raw(lang: "typ", "#your-package.help(\"foo\")") to get the docs for the specified definition printed right in their document. This makes reading up on options and discovering features in your package effortless. After the desired information has been gathered, it's no more than deleting a line of document source code to make the docs vanish into the hidden realms of repositories once again! 328 | 329 | As a demonstration, calling #raw(lang: "typ", "#tidy.help(\"parse-module\")") produces the following (clipped) output into the document. 330 | #{ 331 | set text(size: .8em) 332 | pad(x: 5%, 333 | box( 334 | height: 170pt, clip: true, 335 | box( 336 | height: 180pt, clip: true, 337 | box(tidy.help("parse-module")) 338 | ) 339 | ) 340 | ) 341 | 342 | } 343 | 344 | 345 | This feature supports: 346 | - function and variable definitions, 347 | - definitions defined in nested submodules, e.g., \ #raw(lang: "typ", "#your-package.help(\"sub.bar.foo\")") 348 | - asking only for the parameter description of a function, e.g., \ #raw(lang: "typ", "#your-package.help(\"foo(param1)\")") 349 | - lazy evaluation of doc-comment processing (even loading `tidy` is made lazy). \ _Don't pay for what you don't use!_ 350 | - search through the entire package documentation, e.g., \ #raw(lang: "typ", "#your-package.help(search: \"module\")") 351 | 352 | 353 | == Setup 354 | 355 | If you have already documented your code, adding such a help function will require only little further effort in implementation. In your root library file, add some code of the following kind: 356 | #raw(block: true, lang: "typ", ``` 357 | #let help(..args) = { 358 | ```.text + 359 | ``` 360 | 361 | import "@preview/tidy:0.0.0" 362 | let namespace = ( 363 | ".": read.with("/src/my-package.typ") 364 | ) 365 | tidy.generate-help(namespace: namespace, package-name: "tidy")(..args) 366 | } 367 | ```.text) 368 | First, we set up a `namespace` dictionary that reflects the way that definitions can be accessed by a user. Note that due to import statements that import _from_ a file, this may not reflect the actual file structure of your repository. Take care to provide `read.with()` objects with the filename prepended instead of directly calling `read()`. This allows *tidy* to only lazily read the source files upon a help request from the end user. 369 | 370 | As a more elaborate example, let us look at some library root file for a maths package called `heymath`. 371 | #file-code("heymath.typ", ```typ 372 | #import "vec.typ": vec-add, vec-subtract // import definitions into root 373 | #import "matrix.typ" // submodule "matrix" 374 | 375 | /// ... 376 | #let pi-squared = 9.86960440108935861883 377 | ```) 378 | Our `namespace` dictionary could then look like this: 379 | ```typc 380 | let namespace = ( 381 | ".": (read.with("/heymath.typ"), read.with("/vec.typ")) 382 | "matrix": read.with("/matrix.typ") 383 | "matrix.solve": read.with("/solve.typ") 384 | ) 385 | ``` 386 | Since the symbols from `vec.typ` are imported directly into the library (and are accessible through `heymath.vec-add()` and `heymath.vec-subtract()`), we add this file to the root together with the main library file. Both files will be internally concatenated for doc-comment processing. The content of `matrix.typ`, however, can only be accessed through `heymath.matrix.` (by the user) and so we place `matrix.typ` at the key `matrix`. 387 | For nested submodules, write out the complete name "path" for the key. As an example, we have added `matrix.solve` -- a module that would be imported within `matrix.typ` -- to the code sample above. *It is advised not to change the signature of the help function manually in order to keep consistency between different packages using this features*. 388 | 389 | 390 | == Searching 391 | 392 | It is also possible to search the package documentation via the search argument of the help function: \ #raw(lang: "typ", "#tidy.help(search: \"module\")"). This feature is even more experimental. 393 | #{ 394 | set text(size: .8em) 395 | pad(x: 5%, 396 | box( 397 | height: 170pt, clip: true, 398 | box( 399 | height: 180pt, clip: true, 400 | box(tidy.help(search: "module")) 401 | ) 402 | ) 403 | ) 404 | } 405 | 406 | == Output customization (for end-users) 407 | 408 | The default style for help output should work more or less for light and dark documents but is otherwise not very customizable. This is intended to be changed when user-defined types are available in Typst because these would provide the ideal interface for such customization. Until then, I do not deem it much sense to provide a temporary solution that need. 409 | 410 | == Notes about optimization (for package developers) 411 | 412 | When set up in the form as shown above, the package `tidy` is only imported when a user calls `help` for the first time and not at all if the feature is not used _(don't pay for what you don't use)_. The files themselves are also only read when a definition from a specific submodule in the "namespace" is requested. In the case of _extremely_ long code files, it _could_ make sense to separate the documentation from the implementation by adding "documentation files" that only contain a _declaration_ plus doc-comment for each definition -- with the body left empty. 413 | ```typ 414 | #let my-really-long-algorithm( 415 | /// The inputs for the algorithm. -> array 416 | inputs, 417 | /// Some parameters. -> none | dictionary 418 | parameters: none 419 | ) = { } 420 | ``` 421 | 422 | The advantage is that the source code is not as crowded with (sometimes very long) doc-comments and that doc-comment parsing may get faster. On the downside, there is an increased maintenance overhead due to the need of synchronizing the actual file and the documentation file (especially when the interface of a function changes). 423 | 424 | 425 | 426 | #pagebreak() 427 | = Doc-comment testing 428 | 429 | Tidy supports small-scale doc-comment tests that are executed automatically and throw appropriate error messages when a test fails. 430 | 431 | In every doc-comment, the function #raw(lang: "typc", "test(..tests, scope: (:))") is available. An arbitrary number of tests can be passed in and the evaluation scope may be extended through the `scope` parameter. Any definition exposed to the doc-comment evaluation context through the `scope` parameter passed to #ref-fn("parse-module()") (see @user-defined-symbols) is also accessible in the tests. Let us create a module `num.typ` with the following content: 432 | 433 | ```typ 434 | /// #test( 435 | /// `num.my-square(2) == 4`, 436 | /// `num.my-square(4) == 16`, 437 | /// ) 438 | #let my-square(n) = n * n 439 | ``` 440 | 441 | Parsing and showing the module will run the doc-comment tests. 442 | 443 | ```typ 444 | #import "num.typ" 445 | #let module = tidy.parse-module( 446 | read("num.typ"), 447 | name: "num", 448 | scope: (num: num) 449 | ) 450 | #tidy.show-module(module) // tests are run here 451 | ``` 452 | 453 | // As alternative to using `test()`, the following dedicated shorthand syntax can be used: 454 | 455 | // ```typ 456 | // /// >>> my-square(2) == 4 457 | // /// >>> my-square(4) == 16 458 | // #let my-square(n) = n * n 459 | // ``` 460 | 461 | // When using the shorthand syntax, the error message even shows the line number of the failed test in the corresponding module. 462 | 463 | A few test assertion functions are available to improve readability, simplicity and error messages. Currently, these are `eq(a, b)` for equality tests, `ne(a, b)` for inequality tests and `approx(a, b, eps: 1e-10)` for floating point comparisons. These assertion helper functions are always available within doc-comment tests. 464 | // (with both `test()` and `>>>` syntax). 465 | 466 | Doc-comment tests can be disabled by passing `enable-tests: false` to #ref-fn("show-module()"). 467 | 468 | 469 | 470 | 471 | #pagebreak() 472 | = Function documentation 473 | 474 | Let us now _self-document_ this package: 475 | 476 | #let style = tidy.styles.default 477 | #{ 478 | set text(size: 9pt) 479 | set heading(numbering: none) 480 | show heading.where(level: 3): set text(1.5em) 481 | show heading.where(level: 4): it => { 482 | set text(1.4em) 483 | set align(center) 484 | set block(below: 1.2em) 485 | it 486 | } 487 | 488 | let module = tidy.parse-module( 489 | ( 490 | read("/src/parse-module.typ"), 491 | read("/src/show-module.typ"), 492 | read("/src/helping.typ"), 493 | read("/src/show-example.typ") 494 | ).join("\n"), 495 | name: "tidy", 496 | require-all-parameters: true, 497 | old-syntax: false 498 | ) 499 | tidy.show-module( 500 | module, 501 | style: style, 502 | show-outline: true, 503 | sort-functions: false, 504 | omit-private-parameters: true, 505 | omit-private-definitions: true, 506 | first-heading-level: 3 507 | ) 508 | } 509 | --------------------------------------------------------------------------------