From 0d40bffa517bb8b0eadd1d2eaf3baa95bd4ee5da Mon Sep 17 00:00:00 2001 From: Mc-Zen Date: Sat, 11 Jan 2025 19:18:19 +0100 Subject: [PATCH 1/3] [add] tidy:0.4.1 --- packages/preview/tidy/0.4.1/LICENSE | 21 + packages/preview/tidy/0.4.1/README.md | 177 ++++++++ packages/preview/tidy/0.4.1/src/helping.typ | 313 ++++++++++++++ .../preview/tidy/0.4.1/src/new-parser.typ | 376 ++++++++++++++++ .../preview/tidy/0.4.1/src/old-parser.typ | 406 ++++++++++++++++++ .../preview/tidy/0.4.1/src/parse-module.typ | 168 ++++++++ .../preview/tidy/0.4.1/src/show-example.typ | 233 ++++++++++ .../preview/tidy/0.4.1/src/show-module.typ | 191 ++++++++ packages/preview/tidy/0.4.1/src/styles.typ | 3 + .../preview/tidy/0.4.1/src/styles/default.typ | 225 ++++++++++ .../preview/tidy/0.4.1/src/styles/help.typ | 167 +++++++ .../preview/tidy/0.4.1/src/styles/minimal.typ | 178 ++++++++ packages/preview/tidy/0.4.1/src/testing.typ | 98 +++++ packages/preview/tidy/0.4.1/src/tidy.typ | 23 + packages/preview/tidy/0.4.1/src/utilities.typ | 72 ++++ packages/preview/tidy/0.4.1/typst.toml | 12 + 16 files changed, 2663 insertions(+) create mode 100644 packages/preview/tidy/0.4.1/LICENSE create mode 100644 packages/preview/tidy/0.4.1/README.md create mode 100644 packages/preview/tidy/0.4.1/src/helping.typ create mode 100644 packages/preview/tidy/0.4.1/src/new-parser.typ create mode 100644 packages/preview/tidy/0.4.1/src/old-parser.typ create mode 100644 packages/preview/tidy/0.4.1/src/parse-module.typ create mode 100644 packages/preview/tidy/0.4.1/src/show-example.typ create mode 100644 packages/preview/tidy/0.4.1/src/show-module.typ create mode 100644 packages/preview/tidy/0.4.1/src/styles.typ create mode 100644 packages/preview/tidy/0.4.1/src/styles/default.typ create mode 100644 packages/preview/tidy/0.4.1/src/styles/help.typ create mode 100644 packages/preview/tidy/0.4.1/src/styles/minimal.typ create mode 100644 packages/preview/tidy/0.4.1/src/testing.typ create mode 100644 packages/preview/tidy/0.4.1/src/tidy.typ create mode 100644 packages/preview/tidy/0.4.1/src/utilities.typ create mode 100644 packages/preview/tidy/0.4.1/typst.toml diff --git a/packages/preview/tidy/0.4.1/LICENSE b/packages/preview/tidy/0.4.1/LICENSE new file mode 100644 index 0000000000..42552d3a1f --- /dev/null +++ b/packages/preview/tidy/0.4.1/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Mc-Zen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/preview/tidy/0.4.1/README.md b/packages/preview/tidy/0.4.1/README.md new file mode 100644 index 0000000000..e424e8549b --- /dev/null +++ b/packages/preview/tidy/0.4.1/README.md @@ -0,0 +1,177 @@ + +# Tidy +*Keep it tidy.* + +[![Typst Package](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FMc-Zen%2Ftidy%2Fv0.4.1%2Ftypst.toml&query=%24.package.version&prefix=v&logo=typst&label=package&color=239DAD)](https://typst.app/universe/package/tidy) +[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Mc-Zen/tidy/blob/main/LICENSE) +[![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) +[![User Manual](https://img.shields.io/badge/manual-.pdf-purple)][guide] + + + + +**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! + +---- +**IMPORTANT** + +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, ...)`. + +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). + +---- + +Features: +- **Customizable** output styles. +- Automatically [**preview code examples**](#example). +- **Annotate types** of parameters and return values. +- **Cross-references** to definitions and function parameters. +- Automatically read off default values for named parameters. +- [**Help** feature](#generate-a-help-command-for-you-package) for your package. +- [Doc-tests](#doc-tests). + + +The [guide][guide] fully describes the usage of this module and defines documentation syntax. + +## Usage + +Using `tidy` is as simple as writing some doc-comments and calling: +```typ +#import "@preview/tidy:0.4.1" + +#let docs = tidy.parse-module(read("my-module.typ")) +#tidy.show-module(docs, style: tidy.styles.default) +``` + +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]). + +## Example + +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). + +```typ +/// This function computes the cardinal sine, $sinc(x)=sin(x)/x$. +/// +/// ```example +/// #sinc(0) +/// ``` +/// +/// -> float +#let sinc( + /// The argument for the cardinal sine function. + /// -> int | float + x +) = if x == 0 {1} else {calc.sin(x) / x} +``` + +**tidy** turns this into: + +
+ + ![Tidy example output](https://github.com/user-attachments/assets/e145ca9f-12ab-41ed-a392-80785b29a880) + +
+ + +## Access user-defined functions and images + +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()`: + +```typ +#{ + import "my-module.typ" + let module = tidy.parse-module(read("my-module.typ")) + let an-image = image("img.png") + tidy.show-module( + module, + style: tidy.styles.default, + scope: (my-module: my-module, img: an-image) + ) +} +``` +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! + +## Generate a help command for you package +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: + +```typ +// happily coding, but how do I use this one complex function again? + +#mypackage.help("func") +#mypackage.help("func(param1)") // print only parameter description of param1 +``` + +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]. + +## Doc-tests +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. +```typ +/// #test( +/// `num.my-square(2) == 4`, +/// `num.my-square(4) == 16`, +/// ) +#let my-square(n) = n * n +``` + +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. + + +## Changelog + +### v0.4.1 +_Fixes_ +- Strings containing `"//"` can now be used in default arguments. +- References like `@section` can now link to labels outside the documentation. +- Fixes issues with upcoming Typst 0.13. + +### v0.4.0 +_Major redesign of the documentation syntax_ +- New features + - 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. + - Cross-references to function arguments. + - Support for detecting _curried functions_, i.e., function aliases with prepended arguments using the `.with()` function. + + +### v0.3.0 +_Adds a help feature and more options_ +- New features: + - Help feature. + - `preamble` option for examples (e.g., to add `import` statements). + - more options for `show-module`: `omit-private-definitions`, `omit-private-parameters`, `enable-cross-references`, `local-names` (for configuring language-specific strings). +- Improvements: + - Allow using `show-example()` as standalone. + - Updated type names that changed with Typst 0.8.0, e.g., integer -> int. +- Fixes: + - allow examples with ratio widths if `scale-preview` is not `auto`. + - `show-outline` + - explicitly use `raw(lang: none)` for types and function names. + +### v0.2.0 +- New features: + - Add executable examples to doc-comments. + - Documentation for variables (as well as functions). + - Doc-tests. + - Rainbow-colored types `color` and `gradient`. +- Improvements: + - Allow customization of cross-references through `show-reference()`. + - Allow customization of spacing between functions through styles. + - Allow color customization (especially for the `default` theme). +- Fixes: + - Empty parameter descriptions are omitted (if the corresponding option is set). + - Trim newline characters from parameter descriptions. +- ⚠️ Breaking changes: + - 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. + - (only concerning custom styles) The style functions `show-outline()`, `show-parameter-list`, and `show-type()` now take `style-args` arguments as well. + +### v0.1.0 + +_Initial Release_ + +[guide]: https://github.com/Mc-Zen/tidy/releases/download/v0.4.0/tidy-guide.pdf + +[migration guide]: https://github.com/Mc-Zen/tidy/tree/v0.4.0/docs/migration-to-0.4.0.md diff --git a/packages/preview/tidy/0.4.1/src/helping.typ b/packages/preview/tidy/0.4.1/src/helping.typ new file mode 100644 index 0000000000..40bab57784 --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/helping.typ @@ -0,0 +1,313 @@ +#import "styles.typ" +#import "utilities.typ" +#import "testing.typ" +#import "parse-module.typ": parse-module +#import "show-module.typ": show-module + +#let help-box(content) = { + block( + above: 1em, + inset: 1em, + stroke: rgb("#AAA"), + fill: rgb("#F5F5F544"), { + text(size: 1.8em, [? #smallcaps("help")#h(1fr)?]) + text(.9em, content) + } + ) +} + +#let parse-namespace-modules(entry, old-syntax: false) = { + // "Module" is made up of several files + if type(entry) != array { + entry = (entry,) + } + parse-module(entry.map(x => x()).join("\n"), old-syntax: old-syntax, label-prefix: "help-") +} + +#let search-docs(search, searching, namespace, style, old-syntax: false) = { + if search == "" { return help-box(block[_empty search string_]) } + let search-names = "n" in searching + let search-descriptions = "d" in searching + let search-parameters = "p" in searching + + let search-argument-dict(args) = { + if search in args { return true } + for (key, value) in args { + if "description" in value and search in value.description { return true } + } + return false + } + + let filter = definition => { + (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)) + } + + let definitions = () + let module = parse-namespace-modules(namespace.at("."), old-syntax: old-syntax) + let functions = () + let variables = () + for (name, modules) in namespace { + let module = parse-namespace-modules(modules, old-syntax: old-syntax) + + functions += module.functions.filter(filter) + variables += module.variables.filter(x => search in x.name or search in x.description) + } + module.functions = functions + module.variables = variables + return help-box({ + show search: highlight.with(fill: rgb("#FF28")) + show-module(module, style: style, enable-cross-references: false) + }) +} + + + +#let get-docs( + definition-name, namespace, package-name, style, + onerror: msg => assert(false, message: msg) +) = { + let name = definition-name + let result + if type(name) == function { name = repr(name) } + assert.eq(type(name), str, message: "The definition name has to be a string, found `" + repr(name) + "`") + + let name-components = name.split(".") + name = name-components.pop() + let module-name = name-components.join(".") + + if module-name == none { module-name = "." } + + if module-name not in namespace { + return onerror("The package `" + package-name + "` contains no module `" + module-name + "`") + } + + + let module = parse-namespace-modules(namespace.at(module-name)) + + // We support selecting a specific parameter name (for functions) + let param-name + if "(" in name { + let match = name.match(regex("(\w[\w\d\-_]*)\((.*)\)")) + if match != none { + (name, param-name) = match.captures + if param-name == "" { param-name = none } + definition-name = definition-name.slice(0, definition-name.position("(")) + } + } + + // First check if there is a function with the given name + let definition-doc = module.functions.find(x => x.name == name) + if definition-doc != none { + if param-name != none { // extract only the parameter description + let style-functions = utilities.get-style-functions(style) + + let style-args = ( + style: style-functions, + label-prefix: "", + first-heading-level: 2, + break-param-descriptions: true, + omit-empty-param-descriptions: false, + colors: styles.default.colors, + enable-cross-references: false + ) + + let eval-scope = ( + // Predefined functions that may be called by the user in doc-comment code + example: style-functions.show-example.with( + inherited-scope: module.scope + ), + test: testing.test.with( + inherited-scope: testing.assertations + module.scope, + enable: false + ), + // Internally generated functions + tidy: ( + show-reference: style-functions.show-reference.with(style-args: style-args) + ) + ) + + eval-scope += module.scope + + style-args.scope = eval-scope + + + // Show the docs + if param-name not in definition-doc.args { + if ".." + param-name in definition-doc.args { + param-name = ".." + param-name + } else { + return onerror("The function `" + definition-name + "` has no parameter `" + param-name + "`") + } + } + let info = definition-doc.args.at(param-name) + let types = info.at("types", default: ()) + let description = info.at("description", default: "") + result = block(strong(name), above: 1.8em) + result += (style.show-parameter-block)( + param-name, types, utilities.eval-docstring(description, style-args), + style-args, + show-default: "default" in info, + default: info.at("default", default: none), + ) + } + module.functions = (definition-doc,) + module.variables = () + } else { + let definition-doc = module.variables.find(x => x.name == name) + if definition-doc != none { + assert(param-name == none, message: "Parameters can only be specified for function definitions, not for variables. ") + module.variables = (definition-doc,) + module.functions = () + } else { + + if module-name == "." { + return onerror("The package `" + package-name + "` contains no (documented) definition `" + name + "`") + } else { + return onerror("The module `" + module-name + "` from the package `" + package-name + "` contains no (documented) definition `" + name + "`") + } + } + } + + if result == none { + result = show-module( + module, + style: style, + enable-cross-references: false, + enable-tests: false, + show-outline: false, + ) + } + return result +} + + +/// Generates a `help` function for your package that allows the user to +/// prints references directly into their document while typing. This allows +/// them to easily check the usage and documentation of a function or variable. +#let generate-help( + + /// This dictionary should reflect the "namespace" of the package + /// in a flat dictionary and contain `read.with()` instances for the respective code + /// files. + /// Imagine importing everything from a package, `#import "mypack.typ": *`. How a + /// symbol is accessible now determines how the dictionary should be built. + /// We start with a root key, `(".": read.with("lib.typ"))`. If `lib.typ` imports + /// symbols from other files _into_ its scope, these files should be added to the + /// root along with `lib.typ` by passing an array: + /// ```typ + /// ( + /// ".": (read.with("lib.typ"), read.with("more.typ")), + /// "testing": read.with("testing.typ") + /// ) + /// ``` + /// Here, we already show another case: let `testing.typ` be imported in `lib.typ` + /// but without `*`, so that the symbols are accessed via `testing.`. We therefore + /// add these under a new key. Nested files should be added with multiple + /// dots, e.g., `"testing.float."`. + /// + /// By providing instances of `read()` with the filename prepended, you allow tidy + /// to read the files that are not part of the tidy package but at the same time + /// enable lazy evaluation of the files, i.e., a file is only opened when a + /// definition from this file is requested through `help()`. + /// -> dictionary + namespace: (".": () => ""), + + /// The name of the package. This is required to give helpful error messages when + /// a symbol cannot be found. + /// -> str + package-name: "", + + /// A tidy style that is used for showing parts of the documentation + /// in the help box. It is recommended to leave this at the `help` style which is + /// particularly designed for this purpose. Please post an issue if you have problems + /// or suggestions regarding this style. + /// -> dictionary + style: styles.help, + + /// What to do with errors. By default, an assertion is failed (the document panics). + /// -> function + onerror: msg => assert(false, message: msg), + + /// Whether to use the old parser. + /// -> bool + old-syntax: false +) = { + + let validate-namespace-tree(namespace) = { + let validate-file-reader(file-reader) = { + assert(type(file-reader) == function, message: "The namespace must have instances of `read.with([filename])` as leaves, found " + repr(file-reader)) + } + for (entry, value) in namespace { + if type(value) == array { + for file-reader in value { + validate-file-reader(file-reader) + } + } else if type(value) == dictionary { + validate-namespace-tree(value) + } else { + validate-file-reader(value) + } + } + } + + + validate-namespace-tree(namespace) + + + let help-function = ( + ..args, + search: none, + searching: "ndp", // Enable search of: name, descriptions, parameters + style: style + ) => { + if search == none { + if args.pos().len() == 0 { return none } + let name = args.pos().first() + help-box(get-docs(name, namespace, package-name, style, onerror: onerror)) + } else { + search-docs(search, searching, namespace, style, old-syntax: old-syntax) + } + } + help-function +} + + + + +#let flatten-namespace(namespace) = { + let sub-namespace-name = "" + + let flatten-impl(dict, name) = { + let name-without-dot = name.trim(".") + let flattened-dict = ((name-without-dot): ()) + for (key, value) in dict { + if type(value) == function { value = (value,) } + if key == "." { + flattened-dict.at(name-without-dot) += value + } else if type(value) == array { + flattened-dict.insert(name + key, value) + } else if type(value) == dictionary { + let u = flatten-impl(value, name + key + ".") + flattened-dict += u + } + } + return flattened-dict + } + let flattened-namespace = flatten-impl(namespace, "") + +} + +#flatten-namespace(( + ".": read, + "math": read, + "matrix": ( + ".": (read, read), + "vector": ( + "algebra": read, + "addition": ( + "binary": read + ) + ) + ), + +)) \ No newline at end of file diff --git a/packages/preview/tidy/0.4.1/src/new-parser.typ b/packages/preview/tidy/0.4.1/src/new-parser.typ new file mode 100644 index 0000000000..e3f127bf3f --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/new-parser.typ @@ -0,0 +1,376 @@ + +#let split-once(string, delimiter) ={ + let pos = string.position(delimiter) + if pos == none { return string } + (string.slice(0, pos), string.slice(pos + 1)) +} + +#let parse-argument-list(text) = { + let brace-level = 1 + let literal-mode = none // Whether in ".." + + let args = () + + let arg = "" + let is-named = false // Whether current argument is a named arg + + let previous-char = none // lookbehind of 1 + let count-processed-chars = 1 + + let maybe-split-argument(arg, is-named) = { + if is-named { + return split-once(arg, ":").map(str.trim) + } else { + return (arg.trim(),) + } + } + let skip-line = false + + for c in text { + let ignore-char = false + + if c == "\"" and previous-char != "\\" { + if literal-mode == none { literal-mode = "\"" } + else if literal-mode == "\"" { literal-mode = none } + } else if literal-mode == none { + if c == "(" { brace-level += 1 } + else if c == ")" { brace-level -= 1 } + else if c == "," and brace-level == 1 { + if is-named { + let (name, value) = split-once(arg, ":").map(str.trim) + args.push((name, value)) + } else { + arg = arg.trim() + args.push((arg,)) + } + arg = "" + ignore-char = true + is-named = false + } else if c == ":" and brace-level == 1 { + is-named = true + } else if c == "/" and previous-char == "/" { + skip-line = true + arg = arg.slice(0, -1) + } else if c == "\n" { + skip-line = false + } + } + count-processed-chars += 1 + if brace-level == 0 { + if arg.trim().len() > 0 { + if is-named { + let (name, value) = split-once(arg, ":").map(str.trim) + args.push((name, value.replace("\n", ""))) + } else { + arg = arg.trim().replace("\n", "") + args.push((arg,)) + } + } + break + } + if not (ignore-char or skip-line) { arg += c } + previous-char = c + } + return ( + args: args, + brace-level: brace-level - 1, + processed-chars: count-processed-chars - 1 + ) +} + +#assert.eq( + parse-argument-list("text)"), + (args: (("text",),), brace-level: -1, processed-chars: 5) +) +#assert.eq( + parse-argument-list("pos,"), + (args: (("pos",),), brace-level: 0, processed-chars: 4) +) +#assert.eq( + parse-argument-list("12, 13, a)"), + (args: (("12",), ("13",), ("a",)), brace-level: -1, processed-chars: 10) +) +#assert.eq( + parse-argument-list("a: 2, b: 3)"), + (args: (("a", "2"), ("b", "3")), brace-level: -1, processed-chars: 11) +) +#assert.eq( + parse-argument-list("a: 2 // 2\n)"), + (args: (("a", "2"),), brace-level: -1, processed-chars: 11) +) +#assert.eq( + parse-argument-list("a: 2, // 2\nb)"), + (args: (("a", "2"),("b",)), brace-level: -1, processed-chars: 13) +) + + +#let eval-doc-comment-test((line-number, line), label-prefix: "") = { + if line.starts-with(" >>> ") { + return " #test(`" + line.slice(8) + "`, source-location: (module: \"" + parse-info.label-prefix + "\", line: " + str(line-number) + "))" + } + line +} + + +#let parse-description-and-types(lines, label-prefix: "", first-line-number: 0) = { + + let description = lines + // .enumerate(start: first-line-number) + // .map(eval-doc-comment-test.with(label-prefix: label-prefix)) + .join("\n") + + if description == none { description = "" } + + let types = none + if description.contains("->") { + let parts = description.split("->") + types = parts.last().split("|").map(str.trim) + description = parts.slice(0, -1).join("->") + } + + return ( + description: description.trim(), + types: types + ) +} + +#assert.eq( + parse-description-and-types(("asd",)), + (description: "asd", types: none) +) +#assert.eq( + parse-description-and-types(("->int",)), + (description: "", types: ("int",)) +) +#assert.eq( + parse-description-and-types((" -> int",)), + (description: "", types: ("int",)) +) +#assert.eq( + parse-description-and-types(("abcdefg -> int",)), + (description: "abcdefg", types: ("int",)) +) +#assert.eq( + parse-description-and-types(("abcdefg", "-> int",)), + (description: "abcdefg", types: ("int",)) +) + + + +#let trim-trailing-comments(line) = { + let pos = line.position("//") + if pos == none { return line } + return line.slice(0, pos).trim() +} + +#assert.eq(trim-trailing-comments("1+2+3+4 // 23"), "1+2+3+4") +#assert.eq(trim-trailing-comments("1+2+3+4 // 23 // 3"), "1+2+3+4") + + + + +#let definition-name-regex = regex(`#?let (\w[\w\d\-_]*)\s*(\(?)`.text) + + +#let process-parameters(parameters) = { + let processed-params = (:) + + for param in parameters { + let param-parts = param.name + let (description, types) = parse-description-and-types(param.desc-lines, label-prefix: "") + let param-info = ( + // name: param-parts.first(), + description: description, + ) + if param-parts.len() == 2 { + param-info.default = param-parts.last() + } + if types != none { + param-info.types = types + } + processed-params.insert(param-parts.first(), param-info) + } + processed-params +} + + + +#let process-definition(definition) = { + let (description, types) = parse-description-and-types(definition.description, label-prefix: "") + + if definition.args == none { + definition.remove("args") + if types != none { + definition.type = types.first() + } + } else { + definition.return-types = types + definition.args = process-parameters(definition.args) + } + definition.description = description + definition +} + +#let curry-matcher = regex(" *= *([.\w\d\-_]+)\.with\(") + +#let parameter-parser(state, line) = { + if line.starts-with("///") { + state.unmatched-description.push(line.slice(3)) + } else { + state.unfinished-param += line + "\n" + + let (args, brace-level, processed-chars) = parse-argument-list(state.unfinished-param) + if brace-level == -1 { // parentheses are already closed on this line + state.state = "finished" + // let curry = state.unfinished-param.slice(processed-chars).match(curry-matcher) + // if curry != none { + // state.curry = (name: curry.captures.first(), rest: state.unfinished-param.slice(processed-chars + curry.end)) + // } + } + if args.len() > 0 and (state.unfinished-param.trim("\n").ends-with(",") or state.state == "finished") { + state.params.push((name: args.first(), desc-lines: state.unmatched-description)) + state.unmatched-description = () + state.params += args.slice(1).map(arg => (name: arg, desc-lines: ())) + state.unfinished-param = "" + } + } + return state +} + +#let process-curry-info(info) = { + let pos = info.args + .filter(x => x.name.len() == 1) + .map(x => x.name.at(0)) + let named = info.args + .filter(x => x.name.len() == 2) + .map(x => x.name).to-dict() + + ( + name: info.name, + pos: pos, + named: named + ) +} + + +#let parse(src) = { + let lines = (src.split("\n") + ("",)).map(str.trim) + + let module-description = none + let definitions = () + + + // Parser state + let name = none + let found-code = false // are we still looking for a potential module description? + let args = () + let desc-lines = () + let curry-info = none + + + let param-parser-default = ( + state: "idle", + params: (), + unmatched-description: (), + unfinished-param: "" + ) + let param-parser = param-parser-default + let finished-definition = false + + for line in lines { + if param-parser.state == "finished" { + let curry = param-parser.at("curry", default: none) + + if curry-info != none { + finished-definition = true + curry-info.args = param-parser.params + param-parser = param-parser-default + args = () + } else { + args = param-parser.params + if "curry" in param-parser { + // let curry = param-parser.curry + // curry-info = (name: curry.name) + // param-parser = param-parser-default + // param-parser.state = "running" + // param-parser = parameter-parser(param-parser, curry.rest) + // if param-parser.state == "finished" { + // finished-definition = true + // param-parser = param-parser-default + // } + } else { + finished-definition = true + param-parser = param-parser-default + } + } + } + if param-parser.state == "running" { + param-parser = parameter-parser(param-parser, line) + if param-parser.state == "running" { continue } + } + + if finished-definition { + if name != none { + definitions.push((name: name, description: desc-lines, args: args)) + if curry-info != none { + definitions.at(-1).parent = process-curry-info(curry-info) + curry-info = none + } + } + desc-lines = () + name = none + finished-definition = false + } + + + if line.starts-with("///") { // is a doc-comment line + desc-lines.push(line.slice(3)) + } else if desc-lines != () { + // look for something to attach the doc-comment to + // (a parameter or a definition) + + line = line.trim("#", at: start) + if line.starts-with("let ") and name == none { + + found-code = true + let match = line.match(definition-name-regex) + if match != none { + name = match.captures.first() + if match.captures.at(1) != "" { // it's a function + param-parser.state = "running" + param-parser = parameter-parser(param-parser, line.slice(match.end)) + } else { // it's a variable or a function alias + args = none + finished-definition = true + let p = line.slice(match.end) + + let curry = line.slice(match.end).match(curry-matcher) + if curry != none { + curry-info = (name: curry.captures.first()) + param-parser = parameter-parser(param-parser, line.slice(match.end + curry.end)) + // param-parser.curry = (name: curry.captures.first(), rest: state.unfinished-param.slice(processed-chars + curry.end)) + } + } + } + + } else { // neither /// nor (#)let + if not found-code { + found-code = true + module-description = desc-lines.join("\n") + } + if name == none { + desc-lines = () + } + + } + } + } + + definitions = definitions.map(process-definition) + ( + description: module-description, + functions: definitions.filter(x => "args" in x), + variables: definitions.filter(x => "args" not in x), + ) +} + diff --git a/packages/preview/tidy/0.4.1/src/old-parser.typ b/packages/preview/tidy/0.4.1/src/old-parser.typ new file mode 100644 index 0000000000..a42171f7a4 --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/old-parser.typ @@ -0,0 +1,406 @@ + + +// Matches Typst doc-comment for a function declaration. Example: +// +// // This function does something +// // +// // param1 (str): This is param1 +// // param2 (content, length): This is param2. +// // Yes, it really is. +// #let something(param1, param2) = { +// +// } +// +// 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. +// #let docstring-matcher = regex(`((?:[^\S\r\n]*/{3} ?.*\n)+)[^\S\r\n]*#?let (\w[\w\d\-_]+)`.text) +// #let docstring-matcher = regex(`([^\S\r\n]*///.*(?:\n[^\S\r\n]*///.*)*)\n[^\S\r\n]*#?let (\w[\w\d\-_]*)`.text) +#let docstring-matcher = regex(`(?m)^((?:[^\S\r\n]*///.*\n)+)[^\S\r\n]*#?let (\w[\w\d\-_]*)`.text) +// The regex explained: +// +// First capture group: ([^\S\r\n]*///.*(?:\n[^\S\r\n]*///.*)*) +// is for the docstring. It may start with any whitespace [^\S\r\n]* +// and needs to have /// followed by anything. This is the first line of +// the docstring and we treat it separately only in order to be able to +// match the very first line in the file (which is otherwise tricky here). +// We then match basically the same thing n times: \n[^\S\r\n]*///.*)* +// +// We then want a linebreak (should also have \r here?), arbitrary whitespace +// and the word let or #let: \n[^\S\r\n]*#?let +// +// Second capture group: (\w[\w\d\-_]*) +// Matches the function name (any Typst identifier) + + +// Matches an argument documentation of the form `/// - myparameter (str)`. +#let argument-documentation-matcher = regex(`[^\S\r\n]*/{3} - ([.\w\d\-_]+) \(([\w\d\-_ ,]+)\): ?(.*)`.text) + + + +#let split-once(string, delimiter) ={ + let pos = string.position(delimiter) + if pos == none { return string } + (string.slice(0, pos), string.slice(pos + 1)) +} + +/// #set raw(lang: "typc") +/// Parse a Typst argument list either at +/// - call site, e.g., `f("Timbuktu", value: 23)` or at +/// - declaration, e.g. `let f(place, value: 0)`. +/// +/// This function returns a dictionary `(pos, named, count-processed-chars)` where +/// `count-processed-chars` is the number of processed characters, i.e., the +/// length of the argument list and `pos` and `named` contain the arguments. +/// +/// +/// This function returns `none`, if the argument list is not properly closed. +/// Note, that valid Typst code is expected. +/// +/// *Example: * Calling this function with the following string +/// +/// ``` +/// "#let func(p1, p2: 3pt, p3: (), p4: (entries: ())) = {...}" +/// ``` +/// +/// and index `9` (which points to the opening parenthesis) yields the result +/// ``` +/// ( +/// pos: ("p1", "p5"), +/// named: ( +/// p2: "3pt", +/// p3: "()", +/// p4: "(entries: ())" +/// ) +/// 44, +/// ) +/// ``` +/// +/// This function can deal with +/// - any number of opening and closing parenthesis +/// - string literals +/// We don't deal with: +/// - commented out code (`//` or `/**/`) +/// - raw strings with #raw("``") syntax that contain `"` or `(` or `)` +/// +/// - text (str): String to parse. +/// - index (int): Position of the opening parenthesis of the argument list. +/// -> dictionary +#let parse-argument-list(text, index) = { + if text.len() <= index or text.at(index) != "(" { return none } + if text.len() <= index or text.at(index) != "(" { return ((:), 0) } + index += 1 + let brace-level = 1 + let literal-mode = none // Whether in ".." + + let positional = () + let named = (:) + let sink + + let arg = "" + let is-named = false // Whether current argument is a named arg + + let previous-char = none + let count-processed-chars = 1 + + let maybe-split-argument(arg, is-named) = { + if is-named { + return split-once(arg, ":").map(str.trim) + } else { + return (arg.trim(),) + } + } + + for c in text.slice(index) { + let ignore-char = false + if c == "\"" and previous-char != "\\" { + if literal-mode == none { literal-mode = "\"" } + else if literal-mode == "\"" { literal-mode = none } + } + if literal-mode == none { + if c == "(" { brace-level += 1 } + else if c == ")" { brace-level -= 1 } + else if c == "," and brace-level == 1 { + if is-named { + let (name, value) = split-once(arg, ":").map(str.trim) + named.insert(name, value) + } else { + arg = arg.trim() + if arg.starts-with("..") { sink = arg } + else { positional.push(arg) } + } + arg = "" + ignore-char = true + is-named = false + } else if c == ":" and brace-level == 1 { + is-named = true + } + } + count-processed-chars += 1 + if brace-level == 0 { + if arg.trim().len() > 0 { + if is-named { + let (name, value) = split-once(arg, ":").map(str.trim) + named.insert(name, value) + } else { + arg = arg.trim() + if arg.starts-with("..") { sink = arg } + else { positional.push(arg) } + } + } + break + } + if not ignore-char { arg += c } + previous-char = c + } + if brace-level > 0 { return none } + return ( + pos: positional, + named: named, + sink: sink, + count: count-processed-chars + ) +} + +/// This is similar to @@parse-argument-list but focuses on parameter lists +/// at the declaration site. +/// +/// If the argument list is well-formed, a dictionary is returned with +/// an entry for each parsed +/// argument name. The values are dictionaries that may be empty or +/// have an entry for the key `default` containing a string with the parsed +/// default value for this argument. +/// +/// +/// +/// *Example* \ +/// Let us take the string +/// ```typc +/// "#let func(p1, p2: 3pt, p3: (), p4: (entries: ())) = {...}" +/// ``` +/// Here, we would call `parse-parameter-list(source-code, 9)` and retrieve +/// #pad(x: 1em, ```typc +/// ( +/// p0: (:), +/// p1: (default: "3pt"), +/// p2: (default: "()"), +/// p4: (default: "(entries: ())"), +/// ) +/// ```) +/// +/// - text (str): String to parse. +/// - index (int): Index where the argument list starts. This index should +/// point to the character *next* to the function name, i.e., to the +/// opening brace `(` of the argument list if there is one (note, that +/// function aliases for example produced by `myfunc.where(arg1: 3)` do +/// not have an argument list). +/// -> none, dictionary +#let parse-parameter-list(text, index) = { + let result = parse-argument-list(text, index) + if result == none { return none } + let (pos, named, count) = result + let args = (:) + for arg in arg-strings { + if arg.len() == 1 { + args.insert(arg.at(0), (:)) + } else { + args.insert(arg.at(0), (default: arg.at(1))) + } + } + return (args: args, count: count) +} + + +// Take the result of `parse-argument-list()` and retrieve a list of positional +// and named arguments, respectively. The values are `eval()`ed. +// #let parse-arg-strings(args) = { +// let positional-args = () +// let named-args = (:) +// for arg in args { +// if arg.len() == 1 { +// positional-args.push(eval(arg.at(0))) +// } else { +// named-args.insert(arg.at(0), eval(arg.at(1))) +// } +// } +// return (pos: positional-args, named: named-args) +// } + + + +/// Count the occurences of a single character in a string +/// +/// - string (str): String to investigate. +/// - char (str): Character to count. The string needs to be of length 1. +/// - start (int): Start index. +/// - end (end): Start index. If `-1`, the entire string is searched. +/// -> int +#let count-occurences(string, char, start: 0, end: -1) = { + let count = 0 + if end == -1 { end = string.len() } + for c in string.slice(start, end) { + if c == char { count += 1 } + } + // let i = 0 + // while i < end { + // if string.at(i) == char { count += 1} + // i += 1 + // } + count +} + +#let parse-description-and-documented-args(docstring, parse-info, first-line-number: 0) = { + + let fn-desc = "" + let started-args = false + let documented-args = () + let return-types = none + + for (line-number, line) in docstring.split("\n").enumerate(start: first-line-number) { + // Check if line is a test line -> replace it with a call to #test() + if line.starts-with("/// >>> ") { + line = "/// #test(`" + line.slice(8) + "`, source-location: (module: \"" + line += parse-info.label-prefix + "\", line: " + str(line-number) + "))" + } + let arg-match = line.match(argument-documentation-matcher) + if arg-match == none { + let trimmed-line = line.trim().trim("/") + if trimmed-line.trim().starts-with("->") { + return-types = trimmed-line.trim().slice(2).split(",").map(x => x.trim()) + } else { + if not started-args { fn-desc += trimmed-line + "\n"} + else { + documented-args.last().desc += "\n" + trimmed-line + } + } + } else { + started-args = true + let param-name = arg-match.captures.at(0) + let param-types = arg-match.captures.at(1).split(",").map(x => x.trim()) + let param-desc = arg-match.captures.at(2) + documented-args.push((name: param-name, types: param-types, desc: param-desc)) + } + } + return ( + description: fn-desc, + args: documented-args, + return-types: return-types + ) +} + +#let parse-variable-docstring(source-code, match, parse-info) = { + let docstring = match.captures.at(0) + let name = match.captures.at(1) + + let first-line-number = count-occurences(source-code, "\n", end: match.start) + 1 + + let (description, return-types) = parse-description-and-documented-args(docstring, parse-info, first-line-number: first-line-number) + + let var-specs = ( + name: name, + description: description, + ) + if return-types != none and return-types.len() > 0 { + var-specs.type = return-types.first() + } + return var-specs +} + +#let curry-matcher = regex(" *= *([.\w\d\-_]+)\.with\(") + +#let parse-curried-function(source-code, index) = { + // let docstring = match.captures.at(0) + // let var-name = match.captures.at(1) + let line-end = source-code.slice(index).position("\n") + let k = (line-end, source-code.slice(index)) + if line-end == none { line-end = source-code.len() } + else {line-end += index } + let rest = source-code.slice(index, line-end) + + let match = rest.match(curry-matcher) + if match == none { return none } + + let (pos, named, count) = parse-argument-list(source-code, match.end + index - 1) + return ( + name: match.captures.first(), + pos: pos, + named: named + ) +} + +/// Parse a function doc-comment that has been located in the source code with +/// given match. +/// +/// The return value is a dictionary with the keys +/// - `name` (str): the function name. +/// - `description` (content): the function description. +/// - `args`: A dictionary containing the argument list. +/// - `return-types` (array(str)): A list of possible return types. +/// +/// The entries of the argument list dictionary are +/// - `default` (str): the default value for the argument. +/// - `description` (content): the argument description. +/// - `types` (array(str)): A list of possible argument types. +/// Every entry is optional and the dictionary also contains any non-documented +/// arguments. +/// +/// +/// +/// - source-code (str): The source code containing some documented Typst code. +/// - match (match): A regex match that matches a documentation string. The first +/// capture group should hold the entire, raw docstring and the second capture +/// the function name (excluding the opening parenthesis of the argument list +/// if present). +/// - parse-info (dictionary): +/// -> dictionary +#let parse-function-docstring(source-code, match, parse-info) = { + let docstring = match.captures.at(0) + let fn-name = match.captures.at(1) + + let first-line-number = count-occurences(source-code, "\n", end: match.start) + 1 + + let (description, args: documented-args, return-types) = parse-description-and-documented-args(docstring, parse-info, first-line-number: first-line-number) + + + // let (args, count) = parse-parameter-list(source-code, match.end) + let (pos, named, sink, count) = parse-argument-list(source-code, match.end) + let args = (:) + for arg in pos { args.insert(arg, (:)) } + for (arg, value) in named { args.insert(arg, (default: value)) } + if sink != none { args.insert(sink, (:)) } + + + for arg in documented-args { + if arg.name in args { + args.at(arg.name).description = arg.desc.trim("\n") + args.at(arg.name).types = arg.types + } else { + assert( + false, + message: "The parameter `" + arg.name + "` does not appear in the argument list of the function `" + fn-name + "`" + ) + } + } + if parse-info.require-all-parameters { + for arg in args { + assert( + documented-args.find(x => x.name == arg.at(0)) != none, + message: "The parameter `" + arg.at(0) + "` of the function `" + fn-name + "` is not documented. " + ) + } + } + return ( + name: fn-name, + description: description, + args: args, + return-types: return-types + ) +} + + +#let module-docstring-matcher = regex(`(?m)^((?:[^\S\r\n]*///.*\n)+)\n`.text) + +#let parse-module-docstring(source-code, parse-info) = { + let match = source-code.match(module-docstring-matcher) + if match == none { return none } + let desc = parse-description-and-documented-args(match.captures.first(), parse-info, first-line-number: 0) + return desc.description.trim() +} \ No newline at end of file diff --git a/packages/preview/tidy/0.4.1/src/parse-module.typ b/packages/preview/tidy/0.4.1/src/parse-module.typ new file mode 100644 index 0000000000..00fefc5f7e --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/parse-module.typ @@ -0,0 +1,168 @@ +#import "old-parser.typ" +#import "new-parser.typ" +#import "styles.typ" + + +#let resolve-parents(function-docs) = { + for i in range(function-docs.len()) { + let docs = function-docs.at(i) + if not "parent" in docs { continue } + + let parent = docs.at("parent", default: none) + if parent == none { continue } + + let parent-docs = function-docs.find(x => x.name == parent.name) + if parent-docs == none { continue } + + // Inherit args and return types from parent + docs.args = parent-docs.args + docs.return-types = parent-docs.return-types + + for (arg, value) in parent.named { + assert(arg in docs.args) + docs.args.at(arg).default = value + } + + // Maybe strip some positional arguments + if parent.pos.len() > 0 { + let named-args = docs.args.pairs().filter(((_, info)) => "default" in info) + let positional-args = docs.args.pairs().filter(((_, info)) => not "default" in info) + assert(parent.pos.len() <= positional-args.len(), message: "Too many positional arguments") + positional-args = positional-args.slice(parent.pos.len()) + docs.args = (:) + for (name, info) in positional-args + named-args { + docs.args.insert(name, info) + } + } + function-docs.at(i) = docs + } + return function-docs +} + + +#let old-parse( + content, + label-prefix: "", + require-all-parameters: false, + enable-curried-functions: true +) = { + + let parse-info = ( + label-prefix: label-prefix, + require-all-parameters: require-all-parameters, + ) + + let module-docstring = old-parser.parse-module-docstring(content, parse-info) + + let matches = content.matches(old-parser.docstring-matcher) + let function-docs = () + let variable-docs = () + + for match in matches { + + if content.len() <= match.end or content.at(match.end) != "(" { + let doc = old-parser.parse-variable-docstring(content, match, parse-info) + if enable-curried-functions { + let parent-info = old-parser.parse-curried-function(content, match.end) + if parent-info == none { + variable-docs.push(doc) + } else { + doc.parent = parent-info + if "type" in doc { doc.remove("type") } + doc.args = (:) + function-docs.push(doc) + } + } else { + variable-docs.push(doc) + } + } else { + let function-doc = old-parser.parse-function-docstring(content, match, parse-info) + function-docs.push(function-doc) + } + } + return ( + description: module-docstring, + functions: function-docs, + variables: variable-docs + ) +} + + +/// Parse the doc-comments of a typst module. This function returns a dictionary +/// with the keys +/// - `name`: The module name as a string. +/// - `functions`: A list of function documentations as dictionaries. +/// - `label-prefix`: The prefix for internal labels and references. +/// The label prefix will automatically be the name of the module if not given +/// explicity. +/// +/// The function documentation dictionaries contain the keys +/// - `name`: The function name. +/// - `description`: The function's description. +/// - `args`: A dictionary of info objects for each function argument. +/// +/// These again are dictionaries with the keys +/// - `description` (optional): The description for the argument. +/// - `types` (optional): A list of accepted argument types. +/// - `default` (optional): Default value for this argument. +/// +/// See @show-module for outputting the results of this function. +#let parse-module( + + /// Content of `.typ` file to analyze for docstrings. + /// -> str + content, + + /// The name for the module. + /// -> str + name: "", + + /// The label-prefix for internal function references. If `auto`, the + /// label-prefix name will be the module name. + /// -> auto | str + label-prefix: auto, + + /// Require that all parameters of a functions are documented and fail + /// if some are not. + /// -> bool + require-all-parameters: false, + + /// A dictionary of definitions that are then available in all function + /// and parameter descriptions. + /// -> dictionary + scope: (:), + + /// Code to prepend to all code snippets shown with `#example()`. + /// This can for instance be used to import something from the scope. + /// -> str + preamble: "", + + /// Whether to enable the detection of curried functions. + /// -> bool + enable-curried-functions: true, + + /// Whether to use the old documentation syntax. + /// -> bool + old-syntax: false +) = { + if label-prefix == auto { label-prefix = name + "-" } + + let docs = ( + name: name, + label-prefix: label-prefix, + scope: scope, + preamble: preamble + ) + if old-syntax { + docs += old-parse(content, require-all-parameters: require-all-parameters, label-prefix: label-prefix, enable-curried-functions: enable-curried-functions) + } else { + docs += new-parser.parse(content) + } + // TODO + if enable-curried-functions { + docs.functions = resolve-parents(docs.functions) + } + + + return docs +} diff --git a/packages/preview/tidy/0.4.1/src/show-example.typ b/packages/preview/tidy/0.4.1/src/show-example.typ new file mode 100644 index 0000000000..8fa2105a7a --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/show-example.typ @@ -0,0 +1,233 @@ + +/// Default example layouter used with @show-example. +#let default-layout-example( + /// Code `raw` element to display. + /// -> raw + code, + + /// Rendered preview. + /// -> content + preview, + + /// Direction for laying out the code and preview boxes. + /// -> direction + dir: ltr, + + /// Configures the ratio of the widths of the code and preview boxes. + /// -> int + ratio: 1, + + /// How much to rescale the preview. If set to auto, the the preview is scaled to fit the box. + /// -> auto | ratio + scale-preview: auto, + + /// The code is passed to this function. Use this to customize how the code is shown. + /// -> function + code-block: block, + + /// The preview is passed to this function. Use this to customize how the preview is shown. + /// -> function + preview-block: block, + + /// Spacing between the code and preview boxes. + /// -> length + col-spacing: 5pt +) = { + + let preview-outer-padding = 5pt + let preview-inner-padding = 5pt + + layout(size => context { + let code-width + let preview-width + + if dir.axis() == "vertical" { + code-width = size.width + preview-width = size.width + } else { + code-width = ratio / (ratio + 1) * size.width - 0.5 * col-spacing + preview-width = size.width - code-width - col-spacing + } + + + + let available-preview-width = preview-width - 2 * (preview-outer-padding + preview-inner-padding) + + let preview-size + let scale-preview = scale-preview + + if scale-preview == auto { + preview-size = measure(preview) + assert(preview-size.width != 0pt, message: "The code example has a relative width. Please set `scale-preview` to a fixed ratio, e.g., `100%`") + scale-preview = calc.min(1, available-preview-width / preview-size.width) * 100% + } else { + preview-size = measure(block(preview, width: available-preview-width / (scale-preview / 100%))) + } + + set par(hanging-indent: 0pt) // this messes up some stuff in case someone sets it + + + // We first measure this thing (code + preview) to find out which of the two has + // the larger height. Then we can just set the height for both boxes. + let arrangement(width: 100%, height: auto) = block(width: width, inset: 0pt, stack(dir: dir, spacing: col-spacing, + code-block( + width: code-width, + height: height, + inset: 5pt, + { + set text(size: .9em) + set raw(block: true) + code + } + ), + preview-block( + height: height, width: preview-width, + inset: preview-outer-padding, + box( + width: 100%, + height: if height == auto {auto} else {height - 2*preview-outer-padding}, + fill: white, + inset: preview-inner-padding, + box( + inset: 0pt, + width: preview-size.width * (scale-preview / 100%), + height: preview-size.height * (scale-preview / 100%), + place(scale( + scale-preview, + origin: top + left, + block(preview, height: preview-size.height, width: preview-size.width) + )) + ) + ) + ) + )) + let height = if dir.axis() == "vertical" { auto } + else { measure(arrangement(width: size.width)).height } + arrangement(height: height) + }) +} + + + +/// Takes a `raw` elements and both displays the code and previews the result of +/// its evaluation. +/// +/// The code is by default shown in the language mode `lang: typc` (typst code) +/// if no language has been specified. Code in typst markup lanugage (`lang: typ`) +/// is automatically evaluated in markup mode. +/// +/// Lines in the raw code that start with `>>>` are removed from the outputted code +/// but evaluated in the preview. +#let show-example( + + /// Raw object holding the example code. + /// -> raw + code, + + /// Additional definitions to make available in the evaluation of the preview. + /// -> dictionary + scope: (:), + + /// Code to prepend to the snippet. This can for example be used to configure imports. + /// This is currently only supported in `markup` mode, see @show-example.mode. + /// -> str + preamble: "", + + /// Language mode. Can be `auto`, `"markup"`, or `"code"`. + /// -> auto | str + mode: auto, + + /// This parameter is only used internally. Definitions that are made available to the + /// entire parsed module. + /// -> dictionary + inherited-scope: (:), + + /// Layout function which is passed to code, the preview and all other options, + /// see @show-example.options. + /// -> function + layout: default-layout-example, + + /// Additional options to pass to the layout function. + /// -> any + ..options + +) = { + let displayed-code = code.text + .split("\n") + .filter(x => not x.starts-with(">>>")) + .join("\n") + let executed-code = code.text + .split("\n") + .map(x => x.trim(">>>", at: start)) + .join("\n") + + let lang = if code.has("lang") { code.lang } else { auto } + if mode == auto { + if lang == "typ" { mode = "markup" } + else if lang == "typc" { mode = "code" } + else if lang == "typm" { mode = "math" } + else if lang == auto { mode = "markup" } + } + if lang == auto { + if mode == "markup" { lang = "typ" } + if mode == "code" { lang = "typc" } + if mode == "math" { lang = "typm" } + } + if mode == "code" { + preamble = "" + } + assert(lang in ("typ", "typc", "typm"), message: "Previewing code only supports the languages \"typ\", \"typc\", and \"typm\"") + + layout( + raw(displayed-code, lang: lang, block: true), + [#eval(preamble + executed-code, mode: mode, scope: scope + inherited-scope)], + ..options + ) +} + + + +/// Adds the two languages `example` and `examplec` to `raw` that can be used +/// to render code examples side-by-side with an automatic preview. +/// +/// This function is intended to be used in a show rule +/// ```typ +/// #show: render-example +/// ``` +#let render-examples( + /// Body to apply the show rule to. + /// -> any + body, + + /// Scope + /// -> dictionary + scope: (:), + + /// Layout function which is passed the code, the preview and all other options, + /// see @show-example.options. + /// -> function + layout: default-layout-example +) = { + show raw.where(lang: "example"): it => { + set text(4em / 3) + + show-example( + raw(it.text, block: true, lang: "typ"), + mode: "markup", + scope: scope, + layout: layout, + ) + } + show raw.where(lang: "examplec"): it => { + set text(4em / 3) + + show-example( + raw(it.text, block: true, lang: "typc"), + mode: "code", + scope: scope, + layout: layout, + ..args + ) + } + body +} diff --git a/packages/preview/tidy/0.4.1/src/show-module.typ b/packages/preview/tidy/0.4.1/src/show-module.typ new file mode 100644 index 0000000000..3fb0485f44 --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/show-module.typ @@ -0,0 +1,191 @@ +#import "styles.typ" +#import "utilities.typ" +#import "testing.typ" + + +#let def-state = state("tidy-definitions", (:)) + + +/// Show given module in the given style. +/// This displays all (documented) functions in the module. +/// +/// -> content +#let show-module( + + /// Module documentation information as returned by @parse-module. + /// -> dictionary + module-doc, + + /// The output style to use. This can be a module + /// defining the functions `show-outline`, `show-type`, `show-function`, + /// `show-parameter-list` and `show-parameter-block` or a dictionary with + /// functions for the same keys. + /// -> module | dictionary + style: styles.default, + + /// Level for the module heading. Function names are created as second-level + /// headings and the "Parameters" heading is two levels below the first + /// heading level. + /// -> int + first-heading-level: 2, + + /// Whether to output the name of the module at the top. + /// -> bool + show-module-name: true, + + /// Whether to allow breaking of parameter description blocks. + /// -> bool + break-param-descriptions: false, + + /// Whether to omit description blocks for parameters with empty description. + /// -> bool + omit-empty-param-descriptions: true, + + /// Whether to omit functions and variables starting with an underscore. + /// -> bool + omit-private-definitions: false, + + /// Whether to omit named function arguments starting with an underscore. + /// -> bool + omit-private-parameters: false, + + /// Whether to output an outline of all functions in the module at the beginning. + /// -> bool + show-outline: true, + + /// Function to use to sort the function documentations. With `auto`, they are + /// sorted alphabetically by name and with `none` they are not sorted. Otherwise + /// a function can be passed that each function documentation object is passed to + /// and that should return some key to sort the functions by. + /// -> auto | none | function + sort-functions: auto, + + /// Whether to run doc-comment tests. + /// -> bool + enable-tests: true, + + /// Whether to enable links for cross-references. If set to auto, the style + /// will select its default color set. + /// -> bool + enable-cross-references: true, + + /// Give a dictionary for type and colors and other colors. + /// -> auto | dictionary + colors: auto, + + /// Language-specific names for strings used in the output. Currently, these + /// are `parameters` and `default`. You can for example use: + /// `local-names: (parameters: [Paramètres], default: [défault])`. + /// -> dictionary + local-names: (parameters: [Parameters], default: [Default]) +) = block({ + let label-prefix = module-doc.label-prefix + if sort-functions == auto { + module-doc.functions = module-doc.functions.sorted(key: x => x.name) + } else if type(sort-functions) == function { + module-doc.functions = module-doc.functions.sorted(key: sort-functions) + } + + if omit-private-definitions { + let filter = x => not x.name.starts-with("_") + module-doc.functions = module-doc.functions.filter(filter) + module-doc.variables = module-doc.variables.filter(filter) + } + + + let style-functions = utilities.get-style-functions(style) + + let style-args = ( + style: style-functions, + label-prefix: label-prefix, + first-heading-level: first-heading-level, + break-param-descriptions: break-param-descriptions, + omit-empty-param-descriptions: omit-empty-param-descriptions, + omit-private-parameters: omit-private-parameters, + colors: colors, + enable-cross-references: enable-cross-references, + local-names: local-names, + ) + + + let eval-scope = ( + // Predefined functions that may be called by the user in doc-comment code + example: style-functions.show-example.with( + inherited-scope: module-doc.scope, + preamble: module-doc.preamble + ), + test: testing.test.with( + inherited-scope: testing.assertations + module-doc.scope, + enable: enable-tests + ), + // Internally generated functions + tidy: ( + show-reference: style-functions.show-reference.with(style-args: style-args) + ) + ) + + eval-scope += module-doc.scope + + style-args.scope = eval-scope + + def-state.update(x => { + x + module-doc.functions.map(x => (x.name, 1)).to-dict() + module-doc.variables.map(x => (x.name, 0)).to-dict() + }) + + show ref: it => { + let target = str(it.target) + if target.starts-with(label-prefix){ return it } + if not enable-cross-references { + return raw(target) + } + let defs = def-state.final() + + let base = target + if "." in base { base = base.split(".").first() } + let target-def = defs.at(target, default: none) + let base-def = defs.at(base, default: none) + if target-def == none and base-def == none { return it } + + if target-def == 1 { + target += "()" + } + (eval-scope.tidy.show-reference)(label(label-prefix + target), target) + } + + show raw.where(lang: "example"): it => { + set text(4em / 3) + + (eval-scope.example)( + raw(it.text, block: true, lang: "typ"), + mode: "markup" + ) + } + show raw.where(lang: "examplec"): it => { + set text(4em / 3) + + (eval-scope.example)( + raw(it.text, block: true, lang: "typc"), + mode: "code" + ) + } + + // Show the docs + + if "name" in module-doc and show-module-name and module-doc.name != "" { + heading(module-doc.name, level: first-heading-level) + parbreak() + } + + if show-outline { + (style-functions.show-outline)(module-doc, style-args: style-args) + } + + for (index, fn) in module-doc.functions.enumerate() { + (style-functions.show-function)(fn, style-args) + } + for (index, fn) in module-doc.variables.enumerate() { + (style-functions.show-variable)(fn, style-args) + } +}) + + diff --git a/packages/preview/tidy/0.4.1/src/styles.typ b/packages/preview/tidy/0.4.1/src/styles.typ new file mode 100644 index 0000000000..1a04dbe5ab --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/styles.typ @@ -0,0 +1,3 @@ +#import "styles/default.typ" +#import "styles/minimal.typ" +#import "styles/help.typ" diff --git a/packages/preview/tidy/0.4.1/src/styles/default.typ b/packages/preview/tidy/0.4.1/src/styles/default.typ new file mode 100644 index 0000000000..0edb18af82 --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/styles/default.typ @@ -0,0 +1,225 @@ +#import "../utilities.typ": * + +// Color to highlight function names in +#let function-name-color = rgb("#4b69c6") +#let rainbow-map = ((rgb("#7cd5ff"), 0%), (rgb("#a6fbca"), 33%),(rgb("#fff37c"), 66%), (rgb("#ffa49d"), 100%)) +#let gradient-for-color-types = gradient.linear(angle: 7deg, ..rainbow-map) + +#let default-type-color = rgb("#eff0f3") + +// Colors for Typst types +#let colors = ( + "default": default-type-color, + "content": rgb("#a6ebe6"), + "string": rgb("#d1ffe2"), + "str": rgb("#d1ffe2"), + "none": rgb("#ffcbc4"), + "auto": rgb("#ffcbc4"), + "bool": rgb("#ffedc1"), + "boolean": rgb("#ffedc1"), + "integer": rgb("#e7d9ff"), + "int": rgb("#e7d9ff"), + "float": rgb("#e7d9ff"), + "ratio": rgb("#e7d9ff"), + "length": rgb("#e7d9ff"), + "angle": rgb("#e7d9ff"), + "relative length": rgb("#e7d9ff"), + "relative": rgb("#e7d9ff"), + "fraction": rgb("#e7d9ff"), + "symbol": default-type-color, + "array": default-type-color, + "dictionary": default-type-color, + "arguments": default-type-color, + "selector": default-type-color, + "module": default-type-color, + "stroke": default-type-color, + "function": rgb("#f9dfff"), + "color": gradient-for-color-types, + "gradient": gradient-for-color-types, + "signature-func-name": rgb("#4b69c6"), +) + + +#let colors-dark = { + let k = (:) + let darkify(clr) = clr.darken(30%).saturate(30%) + for (key, value) in colors { + if type(value) == color { + value = darkify(value) + } else if type(value) == gradient { + let map = value.stops().map(((clr, stop)) => (darkify(clr), stop)) + value = value.kind()(..map) + } + k.insert(key, value) + } + k.signature-func-name = rgb("#4b69c6").lighten(40%) + k +} + + + + +#let show-outline(module-doc, style-args: (:)) = { + let prefix = module-doc.label-prefix + let gen-entry(name) = { + if "enable-cross-references" in style-args and style-args.enable-cross-references { + link(label(prefix + name), name) + } else { + name + } + } + if module-doc.functions.len() > 0 { + list(..module-doc.functions.map(fn => gen-entry(fn.name + "()"))) + } + + if module-doc.variables.len() > 0 { + text([Variables:], weight: "bold") + list(..module-doc.variables.map(var => gen-entry(var.name))) + } +} + +// Create beautiful, colored type box +#let show-type(type, style-args: (:)) = { + h(2pt) + let clr = style-args.colors.at(type, default: style-args.colors.at("default", default: default-type-color)) + box(outset: 2pt, fill: clr, radius: 2pt, raw(type, lang: none)) + h(2pt) +} + + + +#let show-parameter-list(fn, style-args: (:)) = { + pad(x: 10pt, { + set text(font: ("DejaVu Sans Mono"), size: 0.85em, weight: 340) + text(fn.name, fill: style-args.colors.at("signature-func-name", default: rgb("#4b69c6"))) + "(" + let inline-args = fn.args.len() < 2 + if not inline-args { "\n " } + let items = () + let args = fn.args + for (name, info) in fn.args { + if style-args.omit-private-parameters and name.starts-with("_") { + continue + } + let types + if "types" in info { + types = ": " + info.types.map(x => show-type(x, style-args: style-args)).join(" ") + } + if style-args.enable-cross-references and not (info.at("description", default: "") == "" and style-args.omit-empty-param-descriptions) { + name = link(label(style-args.label-prefix + fn.name + "." + name.trim(".")), name) + } + items.push(name + types) + } + items.join( if inline-args {", "} else { ",\n "}) + if not inline-args { "\n" } + ")" + if "return-types" in fn and fn.return-types != none { + " -> " + fn.return-types.map(x => show-type(x, style-args: style-args)).join(" ") + } + }) +} + + + +// Create a parameter description block, containing name, type, description and optionally the default value. +#let show-parameter-block( + function-name: none, name, types, content, style-args, + show-default: false, + default: none, +) = block( + inset: 10pt, fill: rgb("ddd3"), width: 100%, + breakable: style-args.break-param-descriptions, + [ + #box(heading(level: style-args.first-heading-level + 3, name)) + #if function-name != none and style-args.enable-cross-references { label(function-name + "." + name.trim(".")) } + #h(1.2em) + #types.map(x => (style-args.style.show-type)(x, style-args: style-args)).join([ #text("or",size:.6em) ]) + + #content + #if show-default [ #parbreak() #style-args.local-names.default: #raw(lang: "typc", default) ] + ] +) + + +#let show-function( + fn, style-args, +) = { + + if style-args.colors == auto { style-args.colors = colors } + + [ + #heading(fn.name, level: style-args.first-heading-level + 1) + #if style-args.enable-cross-references { + label(style-args.label-prefix + fn.name + "()") + } + ] + + eval-docstring(fn.description, style-args) + + block(breakable: style-args.break-param-descriptions, { + heading(style-args.local-names.parameters, level: style-args.first-heading-level + 2) + (style-args.style.show-parameter-list)(fn, style-args: style-args) + }) + + for (name, info) in fn.args { + if style-args.omit-private-parameters and name.starts-with("_") { + continue + } + let types = info.at("types", default: ()) + let description = info.at("description", default: "") + if description == "" and style-args.omit-empty-param-descriptions { continue } + (style-args.style.show-parameter-block)( + name, types, eval-docstring(description, style-args), + style-args, + show-default: "default" in info, + default: info.at("default", default: none), + function-name: style-args.label-prefix + fn.name + ) + } + v(4.8em, weak: true) +} + + + +#let show-variable( + var, style-args, +) = { + if style-args.colors == auto { style-args.colors = colors } + let type = if "type" not in var { none } + else { show-type(var.type, style-args: style-args) } + + stack(dir: ltr, spacing: 1.2em, + if style-args.enable-cross-references [ + #heading(var.name, level: style-args.first-heading-level + 1) + #label(style-args.label-prefix + var.name) + ] else [ + #heading(var.name, level: style-args.first-heading-level + 1) + ], + type + ) + + eval-docstring(var.description, style-args) + v(4.8em, weak: true) +} + + +#let show-reference(label, name, style-args: none) = { + link(label, raw(name, lang: none)) +} + + +#import "../show-example.typ" as example + +#let show-example( + ..args +) = { + + example.show-example( + ..args, + layout: example.default-layout-example.with( + code-block: block.with(radius: 3pt, stroke: .5pt + luma(200)), + preview-block: block.with(radius: 3pt, fill: rgb("#e4e5ea")), + col-spacing: 5pt + ), + ) +} \ No newline at end of file diff --git a/packages/preview/tidy/0.4.1/src/styles/help.typ b/packages/preview/tidy/0.4.1/src/styles/help.typ new file mode 100644 index 0000000000..f87f6d3995 --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/styles/help.typ @@ -0,0 +1,167 @@ +#import "../utilities.typ": * +#import "default.typ" + + +// Color to highlight function names in +#let fn-color = rgb("#1f2a63") +#let fn-color = blue.darken(30%) + +#let default-type-color = rgb("#eff0f3") + + +#let show-outline(module-doc, style-args: (:)) = { + let prefix = module-doc.label-prefix + let items = () + for fn in module-doc.functions { + items.push(fn.name + "()") + // items.push(link(label(prefix + fn.name + "()"), fn.name + "()")) + } + list(..items) +} + +#let show-type(type-name, style-args: (:)) = { + h(2pt) + let clr = style-args.colors.at(type-name, default: style-args.colors.at("default", default: default-type-color)) + if type(clr) == color { + let components = clr.components() + clr = rgb(..components.slice(0, -1), 60%) + } + box(outset: 2pt, fill: clr, radius: 2pt, raw(type-name, lang: none)) + h(2pt) +} + + +#let show-parameter-list(fn, style-args) = { + block(fill: rgb("#d8dbed44"), width: 100%, inset: (x: 0.5em, y: 0.7em), { + set text(font: "DejaVu Sans Mono", size: 0.85em, weight: 340) + text(fn.name) + "(" + let inline-args = fn.args.len() < 5 + if not inline-args { "\n " } + let items = () + for (arg-name, info) in fn.args { + let types + if "types" in info { + types = ": " + info.types.map(x => show-type(x, style-args: style-args)).join(" ") + } + items.push(box(arg-name + types)) + } + items.join( if inline-args {", "} else { ",\n "}) + if not inline-args { "\n" } + ")" + if fn.return-types != none { + box[~-> #fn.return-types.map(x => show-type(x, style-args: style-args)).join(" ")] + } + }) +} + + + +// Create a parameter description block, containing name, type, description and optionally the default value. +#let show-parameter-block( + name, types, content, style-args, + show-default: false, + default: none, +) = block( + inset: 0pt, width: 100%, + breakable: style-args.break-param-descriptions, + [ + #[ + #raw(name, lang: none) + ] + #if types != () [ + (#h(-.2em) + #types.map(x => (style-args.style.show-type)(x, style-args: style-args)).join([ #text("or",size:.6em) ]) + #if show-default [\= #raw(lang: "typc", default) ] + #h(-.2em)) + ] + -- + #content + + ] +) + + +#let show-function( + fn, style-args, +) = { + if style-args.colors == auto { style-args.colors = default.colors } + set par(justify: false, hanging-indent: 1em, first-line-indent: 0em) + + block(breakable: style-args.break-param-descriptions, fill: rgb("#d8dbed44"), + if style-args.enable-cross-references [ + #(style-args.style.show-parameter-list)(fn, style-args) + #label(style-args.label-prefix + fn.name + "()") + ] else [ + #(style-args.style.show-parameter-list)(fn, style-args) + ]) + pad(x: 0em, eval-docstring(fn.description, style-args)) + + let parameter-block + + for (name, info) in fn.args { + let types = info.at("types", default: ()) + let description = info.at("description", default: "") + if description == "" and style-args.omit-empty-param-descriptions { continue } + parameter-block += (style-args.style.show-parameter-block)( + name, types, eval-docstring(description, style-args), + style-args, + show-default: "default" in info, + default: info.at("default", default: none), + ) + } + + if parameter-block != none { + [*Parameters:*] + parameter-block + } + v(2em, weak: true) +} + + +#let show-variable( + var, style-args, +) = { + if style-args.colors == auto { style-args.colors = default.colors } + set par(justify: false, hanging-indent: 1em, first-line-indent: 0em) + + let type = if "type" not in var { none } + else { show-type(var.type, style-args: style-args) } + + block(breakable: style-args.break-param-descriptions, fill: rgb("#d8dbed44"), width: 100%, inset: (x: 0.5em, y: 0.7em), + stack(dir: ltr, spacing: 1.2em, + if style-args.enable-cross-references [ + #set text(font: "DejaVu Sans Mono", size: 0.85em, weight: 340) + #text(var.name) + #label(style-args.label-prefix + var.name) + ] else [ + #set text(font: "DejaVu Sans Mono", size: 0.85em, weight: 340) + #text(var.name) + ], + type + ) + ) + pad(x: 0em, eval-docstring(var.description, style-args)) + + v(2em, weak: true) +} + + +#let show-reference(label, name, style-args: none) = { + link(label, raw(name, lang: none)) +} + +#import "../show-example.typ" as example + +#let show-example( + ..args +) = { + + example.show-example( + ..args, + layout: example.default-layout-example.with( + code-block: block.with(radius: 3pt, stroke: .5pt + luma(200)), + preview-block: block.with(radius: 3pt, fill: rgb("#e4e5ea")), + col-spacing: 5pt + ), + ) +} \ No newline at end of file diff --git a/packages/preview/tidy/0.4.1/src/styles/minimal.typ b/packages/preview/tidy/0.4.1/src/styles/minimal.typ new file mode 100644 index 0000000000..88250e6efe --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/styles/minimal.typ @@ -0,0 +1,178 @@ +#import "../utilities.typ": * + + +// Color to highlight function names in +#let fn-color = rgb("#1f2a63") + +#let get-type-color(type) = rgb("#eff0f3") + + +#let show-outline(module-doc, style-args: (:)) = { + let prefix = module-doc.label-prefix + let gen-entry(name) = { + if style-args.enable-cross-references { + link(label(prefix + name), name) + } else { + name + } + } + if module-doc.functions.len() > 0 { + list(..module-doc.functions.map(fn => gen-entry(fn.name + "()"))) + } + + if module-doc.variables.len() > 0 { + text([Variables:], weight: "bold") + list(..module-doc.variables.map(var => gen-entry(var.name))) + } +} + +// Create beautiful, colored type box +#let show-type(type, style-args: (:)) = { + h(2pt) + box(outset: 2pt, fill: get-type-color(type), radius: 2pt, raw(type, lang: none)) + h(2pt) +} + + + +#let show-parameter-list(fn, style-args) = { + block(fill: rgb("#d8dbed"), width: 100%, inset: (x: 0.5em, y: 0.7em), { + set text(font: "Cascadia Mono", size: 0.85em, weight: 340) + text(fn.name, fill: fn-color) + "(" + let inline-args = fn.args.len() < 5 + if not inline-args { "\n " } + let items = () + for (name, info) in fn.args { + if style-args.omit-private-parameters and name.starts-with("_") { + continue + } + let types + if "types" in info { + types = ": " + info.types.map(x => show-type(x)).join(" ") + } + if style-args.enable-cross-references and not (info.at("description", default: "") == "" and style-args.omit-empty-param-descriptions) { + name = link(label(style-args.label-prefix + fn.name + "." + name.trim(".")), name) + } + items.push(box(name + types)) + } + items.join( if inline-args {", "} else { ",\n "}) + if not inline-args { "\n" } + ")" + if fn.return-types != none { + box[~-> #fn.return-types.map(x => show-type(x)).join(" ")] + } + }) +} + + + +// Create a parameter description block, containing name, type, description and optionally the default value. +#let show-parameter-block( + name, types, content, style-args, + show-default: false, + default: none, + function-name: none +) = block( + inset: 0pt, width: 100%, + breakable: style-args.break-param-descriptions, + [ + #[ + #set text(fill: fn-color) + #raw(name, lang: none) + #if function-name != none and style-args.enable-cross-references { label(function-name + "." + name.trim(".")) } + ] + (#h(-.2em) + #types.map(x => (style-args.style.show-type)(x)).join([ #text("or",size:.6em) ]) + #if show-default [\= #raw(lang: "typc", default) ] + #h(-.2em)) -- + #content + + ] +) + + +#let show-function( + fn, style-args, +) = { + set par(justify: false, hanging-indent: 1em, first-line-indent: 0em) + + block(breakable: style-args.break-param-descriptions)[ + #(style-args.style.show-parameter-list)(fn, style-args) + #if style-args.enable-cross-references { + label(style-args.label-prefix + fn.name + "()") + } + ] + pad(x: 0em, eval-docstring(fn.description, style-args)) + + let parameter-block + + for (name, info) in fn.args { + if style-args.omit-private-parameters and name.starts-with("_") { + continue + } + let types = info.at("types", default: ()) + let description = info.at("description", default: "") + if description == "" and style-args.omit-empty-param-descriptions { continue } + parameter-block += (style-args.style.show-parameter-block)( + name, types, eval-docstring(description, style-args), + style-args, + show-default: "default" in info, + default: info.at("default", default: none), + function-name: style-args.label-prefix + fn.name + ) + } + + if parameter-block != none { + [*#style-args.local-names.parameters:*] + parameter-block + } + v(4em, weak: true) +} + + +#let show-variable( + var, style-args, +) = { + set par(justify: false, hanging-indent: 1em, first-line-indent: 0em) + + let type = if "type" not in var { none } + else { show-type(var.type, style-args: style-args) } + + block(breakable: style-args.break-param-descriptions, fill: rgb("#d8dbed"), width: 100%, inset: (x: 0.5em, y: 0.7em), + stack(dir: ltr, spacing: 1.2em, + if style-args.enable-cross-references [ + #set text(font: "Cascadia Mono", size: 0.85em, weight: 340) + #text(var.name, fill: fn-color) + #label(style-args.label-prefix + var.name) + ] else [ + #set text(font: "Cascadia Mono", size: 0.85em, weight: 340) + #text(var.name, fill: fn-color) + ], + type + ) + ) + pad(x: 0em, eval-docstring(var.description, style-args)) + + v(4em, weak: true) +} + + +#let show-reference(label, name, style-args: none) = { + link(label, raw(name, lang: none)) +} + +#import "../show-example.typ" as example + +#let show-example( + ..args +) = { + + example.show-example( + ..args, + layout: example.default-layout-example.with( + code-block: block.with(stroke: .5pt + fn-color), + preview-block: block.with(stroke: .5pt + fn-color), + col-spacing: 0pt + ), + ) +} \ No newline at end of file diff --git a/packages/preview/tidy/0.4.1/src/testing.typ b/packages/preview/tidy/0.4.1/src/testing.typ new file mode 100644 index 0000000000..0b75f99b32 --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/testing.typ @@ -0,0 +1,98 @@ + +/// Check for equality. +#let eq(a, b) = { + if a == b { return (true,) } + else { + return (false, repr(a) + " != " + repr(b)) + } +} + +/// Check for inequality. +#let ne(a, b) = { + if a != b { return (true,) } + else { + return (false, repr(a) + " == " + repr(b)) + } +} + +/// Check for approximate equality. +#let approx(a, b, eps: 1e-10) = { + if calc.abs(a - b) < eps { return (true,) } + else { + return (false, str(a) + " !≈ " + str(b)) + } +} + +#let assertations = ( + eq: eq, + ne: ne, + approx: approx +) + + +#let get-source-info-str(source-location) = { + if source-location == none { return none } + return "(" + source-location.module + ":" + str(source-location.line) + ")" +} + + + +/// Implementation for doc-comment tests. All tests are run immediately. Fails if +/// at least one test did not succeed. +/// +/// This function is made available in all doc-comments under the name 'test'. +#let test( + + /// Tests to run in form of raw objects. + /// -> any + ..tests, + + /// Additional definitions to make available for the evaluated test code. + /// -> dictionary + scope: (:), + + /// Definitions that are made available to the entire parsed module including + /// the test functions. This parameter is only used internally. + /// -> dictionary + inherited-scope: (:), + + /// Information about the location of the test source code. Should contain + /// values for the keys `module` and `line`. This parameter is only used internally. + /// -> dictionary + source-location: none, + + /// When set to `false`, the tests are ignored. + /// -> bool + enable: true + +) = { + if not enable { return } + let source-info = get-source-info-str(source-location) + + for test in tests.pos() { + let result = eval(test.text, scope: scope + inherited-scope) + let result-type = type(result) + + if result-type == array { + if not result.at(0) { + assert( + false, + message: "Failed test " + source-info + ": " + + result.at(1) + "\nin " + test.text + ) + } + } else if result-type == bool { + if not result { + let msg = test.text + assert(false, message: "Failed test " + source-info + ": " + msg) + } + } else { + assert( + false, + message: "Test \"" + test.text + + "\" at " + source-info + + " did not result in a boolean expression" + ) + } + } +} diff --git a/packages/preview/tidy/0.4.1/src/tidy.typ b/packages/preview/tidy/0.4.1/src/tidy.typ new file mode 100644 index 0000000000..32921e3305 --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/tidy.typ @@ -0,0 +1,23 @@ +// Source code for the typst-doc package + + +#import "styles.typ" +#import "old-parser.typ" as tidy-parse +#import "utilities.typ" +#import "testing.typ" +#import "show-example.typ" as show-example: render-examples +#import "parse-module.typ": parse-module +#import "show-module.typ": show-module +#import "helping.typ" as helping: generate-help + + +#let help(..args) = { + let namespace = ( + ".": ( + read.with("/src/parse-module.typ"), + read.with("/src/show-module.typ"), + read.with("/src/helping.typ"), + ) + ) + generate-help(namespace: namespace, package-name: "tidy")(..args) +} diff --git a/packages/preview/tidy/0.4.1/src/utilities.typ b/packages/preview/tidy/0.4.1/src/utilities.typ new file mode 100644 index 0000000000..807996f50f --- /dev/null +++ b/packages/preview/tidy/0.4.1/src/utilities.typ @@ -0,0 +1,72 @@ + +// Matches doc-comment references of the form `@@otherfunc` or `@@otherfunc()`. +#let reference-matcher = regex(`@@([\w\d\-_\)\(]+)`.text) + + +/// Take a documentation string (for example a function or parameter +/// description) and process doc-comment cross-references (starting with `@@`), +/// turning them into links. +#let process-references( + + /// Source code. -> str + text, + + /// -> dictionary + info + +) = { + return text.replace(reference-matcher, match => { + let target = match.captures.at(0) + if info.enable-cross-references { + return "#(tidy.show-reference)(label(\"" + info.label-prefix + target + "\"), \"" + target + "\")" + } else { + return target + } + }) +} + + + +/// Evaluate a doc-comment description (i.e., a function or parameter description) +/// while processing cross-references (@@...) and providing the scope to the +/// evaluation context. +#let eval-docstring( + + /// Doc-comment to evaluate. -> str + docstring, + + /// Object holding information for cross-reference processing and evaluation scope. + /// -> dictionary + info + +) = { + let scope = info.scope + let content = process-references(docstring.trim(), info) + eval(content, mode: "markup", scope: scope) +} + + +#let get-style-functions(style) = { + // Default implementations for some style functions + let show-reference(label, name, style-args) = link(label, raw(name)) + + import "styles.typ" + let show-example = styles.default.show-example + let show-variable = styles.default.show-variable + + let style-functions = style + if type(style) == module { + import style: * + style-functions = ( + show-outline: show-outline, + show-type: show-type, + show-function: show-function, + show-parameter-list: show-parameter-list, + show-parameter-block: show-parameter-block, + show-reference: show-reference, + show-example: show-example, + show-variable: show-variable, + ) + } + return style-functions +} \ No newline at end of file diff --git a/packages/preview/tidy/0.4.1/typst.toml b/packages/preview/tidy/0.4.1/typst.toml new file mode 100644 index 0000000000..03368cc5e8 --- /dev/null +++ b/packages/preview/tidy/0.4.1/typst.toml @@ -0,0 +1,12 @@ +[package] +name = "tidy" +version = "0.4.1" +entrypoint = "src/tidy.typ" +authors = ["Mc-Zen "] +license = "MIT" +description = "Documentation generator for Typst code in Typst." + +repository = "https://github.com/Mc-Zen/tidy" +categories = ["utility", "scripting", "model"] +compiler = "0.11.0" +exclude = ["/docs/*"] \ No newline at end of file From 9da327cd6187b1570cd0845acaab954d2d655c5f Mon Sep 17 00:00:00 2001 From: Mc-Zen Date: Sat, 11 Jan 2025 19:27:59 +0100 Subject: [PATCH 2/3] [update] link to package guide --- packages/preview/tidy/0.4.1/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/preview/tidy/0.4.1/README.md b/packages/preview/tidy/0.4.1/README.md index e424e8549b..cc3d6da6d6 100644 --- a/packages/preview/tidy/0.4.1/README.md +++ b/packages/preview/tidy/0.4.1/README.md @@ -172,6 +172,6 @@ _Adds a help feature and more options_ _Initial Release_ -[guide]: https://github.com/Mc-Zen/tidy/releases/download/v0.4.0/tidy-guide.pdf +[guide]: https://github.com/Mc-Zen/tidy/releases/download/v0.4.1/tidy-guide.pdf -[migration guide]: https://github.com/Mc-Zen/tidy/tree/v0.4.0/docs/migration-to-0.4.0.md +[migration guide]: https://github.com/Mc-Zen/tidy/tree/v0.4.1/docs/migration-to-0.4.0.md From 6bfcf92e1cd1fb5c6e05e70c150fac1501e94058 Mon Sep 17 00:00:00 2001 From: Mc-Zen Date: Sun, 12 Jan 2025 19:13:26 +0100 Subject: [PATCH 3/3] [update] license --- packages/preview/tidy/0.4.1/LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preview/tidy/0.4.1/LICENSE b/packages/preview/tidy/0.4.1/LICENSE index 42552d3a1f..7eec4c0ade 100644 --- a/packages/preview/tidy/0.4.1/LICENSE +++ b/packages/preview/tidy/0.4.1/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Mc-Zen +Copyright (c) 2023-2025 Mc-Zen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal