├── fir ├── version.yue ├── generic │ ├── index.md │ ├── backend.yue │ ├── emit │ │ ├── markdown.yue │ │ └── tests.yue │ └── parser.yue ├── error.yue └── file.yue ├── .gitignore ├── docs ├── fir-logo.png ├── generic │ ├── index.md │ ├── backend.md │ ├── parser.md │ └── emit │ │ ├── markdown.md │ │ └── tests.md ├── stylesheets │ └── extra.css ├── tutorial │ ├── alfons.md │ ├── tests.md │ ├── CLI.md │ └── index.md ├── index.md └── examples │ └── generic-backend.html ├── .gitattributes ├── examples ├── Fir.docs.moon ├── testable.moon ├── Fir.test.moon └── testable.test.moon ├── TODO.md ├── Fir.moon ├── rockspecs ├── fir-1.0-1.rockspec ├── fir-1.1-1.rockspec ├── fir-1.2-1.rockspec ├── fir-1.3-1.rockspec ├── fir-1.1.1-1.rockspec ├── fir-1.1.2-1.rockspec ├── fir-1.2.1-1.rockspec ├── fir-1.3.1-1.rockspec ├── fir-2.0.0-1.rockspec └── fir-2.0.0-2.rockspec ├── rock.yml ├── ROADMAP.md ├── LICENSE.md ├── mkdocs.yml ├── Alfons.yue ├── README.md ├── alfons └── tasks │ └── fir.yue └── bin └── fir.yue /fir/version.yue: -------------------------------------------------------------------------------- 1 | "2.0.0" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .cache 3 | -------------------------------------------------------------------------------- /docs/fir-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daelvn/fir/HEAD/docs/fir-logo.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.lua linguist-generated 2 | *.rockspec linguist-generated 3 | *.yue linguist-language=MoonScript 4 | docs/* linguist-documentation 5 | -------------------------------------------------------------------------------- /examples/Fir.docs.moon: -------------------------------------------------------------------------------- 1 | config: 2 | -- general config 3 | format: "markdown" -- fir.generic.emit.markdown 4 | name: "Fir/tests" 5 | 6 | -- language (moon/yue) 7 | language: {single: "--"} 8 | 9 | -- files 10 | input: {"examples/*.test.lua"} 11 | transform: (path) -> (path\match "examples/(.+)%.test.lua") .. ".md" 12 | output: "examples" 13 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## To-do 2 | 3 | ``` 4 | ├─ /home/daelvn/dev/fir/bin/fir.mp line 12: TODO Integrate this into alfons 5 | ├─ /home/daelvn/dev/fir/docs/cli.md line 19: TODO include backend and emit options 6 | ├─ /home/daelvn/dev/fir/docs/index.html line 5: TODO add opengraph meta tags 7 | └─ /home/daelvn/dev/fir/fir/generic/parser.mp line 76: TODO luassert tests (-- %?^?[TtFfenu:=~] .+) 8 | 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/testable.moon: -------------------------------------------------------------------------------- 1 | ---# examples.testable #--- 2 | -- Example document to generate tests and documentation from them 3 | 4 | --# main #-- 5 | 6 | --- @function id :: a -> a 7 | --- Identity function 8 | -- 9 | -- ?? 2 == id 1 10 | export id = (a) -> a 11 | 12 | --- @function map :: (a -> b) -> [a] -> [b] 13 | --- Map function 14 | -- 15 | -- !! array = { 1, 2, 3 } 16 | -- ??~ ((map id) array), array 17 | export map = (fn) -> (l) -> [fn a for a in *l] 18 | -------------------------------------------------------------------------------- /examples/Fir.test.moon: -------------------------------------------------------------------------------- 1 | config: 2 | -- general config 3 | format: "tests" -- 4 | name: "Fir/tests" 5 | emit: 6 | print: 'tap' 7 | auto_unpack: true 8 | 9 | -- language (moon/yue) 10 | language: {single: "--"} 11 | 12 | -- files 13 | input: {"examples/testable.moon"} 14 | transform: (path) -> (path\match "examples/(.+)%.moon") .. ".test.moon" 15 | output: "examples" 16 | ignore: { -- formatted for filekit.fromGlob 17 | "*.test.lua" 18 | "module/*" 19 | } 20 | 21 | -- tests 22 | -- emit: 23 | -- unpack: { "field" } 24 | -- docs: false 25 | -------------------------------------------------------------------------------- /Fir.moon: -------------------------------------------------------------------------------- 1 | config: 2 | name: "Fir" 3 | -- general config 4 | format: "markdown" -- fir.generic.emit.markdown 5 | emit: 6 | symbols_in_code: true 7 | inline_type: true 8 | tabs: 9 | use: "pymdownx" 10 | 11 | -- language (moon/yue) 12 | language: {single: "--"} 13 | 14 | -- files 15 | input: { 16 | "fir/**.yue" 17 | { "README.md", verbatim: true } 18 | { "fir/**.md", verbatim: true } 19 | } -- formatted for lpath.fs.glob 20 | 21 | transform: (path) -> 22 | if path\match "yue$" 23 | (path\match "fir/(.+)%.yue") .. ".md" 24 | elseif path\match "README.md" 25 | "index.md" 26 | else 27 | path\match "fir/(.+)" 28 | 29 | output: "docs" 30 | 31 | ignore: { 32 | "fir/version.yue" 33 | "fir/file.yue" 34 | } 35 | -------------------------------------------------------------------------------- /rockspecs/fir-1.0-1.rockspec: -------------------------------------------------------------------------------- 1 | build = { 2 | install = { 3 | bin = { 4 | fir = "bin/fir.lua" 5 | } 6 | }, 7 | modules = { 8 | ["fir.file"] = "fir/file.lua", 9 | ["fir.generic.backend"] = "fir/generic/backend.lua", 10 | ["fir.generic.emit.markdown"] = "fir/generic/emit/markdown.lua", 11 | ["fir.generic.parser"] = "fir/generic/parser.lua", 12 | ["fir.version"] = "fir/version.lua" 13 | }, 14 | type = "builtin" 15 | } 16 | dependencies = { 17 | "lua >= 5.1", 18 | "argparse", 19 | "ansikit", 20 | "filekit" 21 | } 22 | description = { 23 | detailed = "fir extracts documentation comments from source code and builds trees out of them, so you can turn them into any other format.\n", 24 | homepage = "https://github.com/daelvn/fir", 25 | summary = "A language-agnostic documentation generator." 26 | } 27 | package = "fir" 28 | rockspec_format = "3.0" 29 | source = { 30 | tag = "v1.0", 31 | url = "git://github.com/daelvn/fir.git" 32 | } 33 | version = "1.0-1" 34 | -------------------------------------------------------------------------------- /rockspecs/fir-1.1-1.rockspec: -------------------------------------------------------------------------------- 1 | build = { 2 | install = { 3 | bin = { 4 | fir = "bin/fir.lua" 5 | } 6 | }, 7 | modules = { 8 | ["fir.file"] = "fir/file.lua", 9 | ["fir.generic.backend"] = "fir/generic/backend.lua", 10 | ["fir.generic.emit.markdown"] = "fir/generic/emit/markdown.lua", 11 | ["fir.generic.parser"] = "fir/generic/parser.lua", 12 | ["fir.version"] = "fir/version.lua" 13 | }, 14 | type = "builtin" 15 | } 16 | dependencies = { 17 | "lua >= 5.1", 18 | "argparse", 19 | "ansikit", 20 | "filekit" 21 | } 22 | description = { 23 | detailed = "fir extracts documentation comments from source code and builds trees out of them, so you can turn them into any other format.\n", 24 | homepage = "https://github.com/daelvn/fir", 25 | summary = "A language-agnostic documentation generator." 26 | } 27 | package = "fir" 28 | rockspec_format = "3.0" 29 | source = { 30 | tag = "v1.1", 31 | url = "git://github.com/daelvn/fir.git" 32 | } 33 | version = "1.1-1" 34 | -------------------------------------------------------------------------------- /rockspecs/fir-1.2-1.rockspec: -------------------------------------------------------------------------------- 1 | build = { 2 | install = { 3 | bin = { 4 | fir = "bin/fir.lua" 5 | } 6 | }, 7 | modules = { 8 | ["fir.file"] = "fir/file.lua", 9 | ["fir.generic.backend"] = "fir/generic/backend.lua", 10 | ["fir.generic.emit.markdown"] = "fir/generic/emit/markdown.lua", 11 | ["fir.generic.parser"] = "fir/generic/parser.lua", 12 | ["fir.version"] = "fir/version.lua" 13 | }, 14 | type = "builtin" 15 | } 16 | dependencies = { 17 | "lua >= 5.1", 18 | "argparse", 19 | "ansikit", 20 | "filekit" 21 | } 22 | description = { 23 | detailed = "fir extracts documentation comments from source code and builds trees out of them, so you can turn them into any other format.\n", 24 | homepage = "https://github.com/daelvn/fir", 25 | summary = "A language-agnostic documentation generator." 26 | } 27 | package = "fir" 28 | rockspec_format = "3.0" 29 | source = { 30 | tag = "v1.2", 31 | url = "git://github.com/daelvn/fir.git" 32 | } 33 | version = "1.2-1" 34 | -------------------------------------------------------------------------------- /rockspecs/fir-1.3-1.rockspec: -------------------------------------------------------------------------------- 1 | build = { 2 | install = { 3 | bin = { 4 | fir = "bin/fir.lua" 5 | } 6 | }, 7 | modules = { 8 | ["fir.file"] = "fir/file.lua", 9 | ["fir.generic.backend"] = "fir/generic/backend.lua", 10 | ["fir.generic.emit.markdown"] = "fir/generic/emit/markdown.lua", 11 | ["fir.generic.parser"] = "fir/generic/parser.lua", 12 | ["fir.version"] = "fir/version.lua" 13 | }, 14 | type = "builtin" 15 | } 16 | dependencies = { 17 | "lua >= 5.1", 18 | "argparse", 19 | "ansikit", 20 | "filekit" 21 | } 22 | description = { 23 | detailed = "fir extracts documentation comments from source code and builds trees out of them, so you can turn them into any other format.\n", 24 | homepage = "https://github.com/daelvn/fir", 25 | summary = "A language-agnostic documentation generator." 26 | } 27 | package = "fir" 28 | rockspec_format = "3.0" 29 | source = { 30 | tag = "v1.3", 31 | url = "git://github.com/daelvn/fir.git" 32 | } 33 | version = "1.3-1" 34 | -------------------------------------------------------------------------------- /rockspecs/fir-1.1.1-1.rockspec: -------------------------------------------------------------------------------- 1 | build = { 2 | install = { 3 | bin = { 4 | fir = "bin/fir.lua" 5 | } 6 | }, 7 | modules = { 8 | ["fir.file"] = "fir/file.lua", 9 | ["fir.generic.backend"] = "fir/generic/backend.lua", 10 | ["fir.generic.emit.markdown"] = "fir/generic/emit/markdown.lua", 11 | ["fir.generic.parser"] = "fir/generic/parser.lua", 12 | ["fir.version"] = "fir/version.lua" 13 | }, 14 | type = "builtin" 15 | } 16 | dependencies = { 17 | "lua >= 5.1", 18 | "argparse", 19 | "ansikit", 20 | "filekit" 21 | } 22 | description = { 23 | detailed = "fir extracts documentation comments from source code and builds trees out of them, so you can turn them into any other format.\n", 24 | homepage = "https://github.com/daelvn/fir", 25 | summary = "A language-agnostic documentation generator." 26 | } 27 | package = "fir" 28 | rockspec_format = "3.0" 29 | source = { 30 | tag = "v1.1.1", 31 | url = "git://github.com/daelvn/fir.git" 32 | } 33 | version = "1.1.1-1" 34 | -------------------------------------------------------------------------------- /rockspecs/fir-1.1.2-1.rockspec: -------------------------------------------------------------------------------- 1 | build = { 2 | install = { 3 | bin = { 4 | fir = "bin/fir.lua" 5 | } 6 | }, 7 | modules = { 8 | ["fir.file"] = "fir/file.lua", 9 | ["fir.generic.backend"] = "fir/generic/backend.lua", 10 | ["fir.generic.emit.markdown"] = "fir/generic/emit/markdown.lua", 11 | ["fir.generic.parser"] = "fir/generic/parser.lua", 12 | ["fir.version"] = "fir/version.lua" 13 | }, 14 | type = "builtin" 15 | } 16 | dependencies = { 17 | "lua >= 5.1", 18 | "argparse", 19 | "ansikit", 20 | "filekit" 21 | } 22 | description = { 23 | detailed = "fir extracts documentation comments from source code and builds trees out of them, so you can turn them into any other format.\n", 24 | homepage = "https://github.com/daelvn/fir", 25 | summary = "A language-agnostic documentation generator." 26 | } 27 | package = "fir" 28 | rockspec_format = "3.0" 29 | source = { 30 | tag = "v1.1.2", 31 | url = "git://github.com/daelvn/fir.git" 32 | } 33 | version = "1.1.2-1" 34 | -------------------------------------------------------------------------------- /rockspecs/fir-1.2.1-1.rockspec: -------------------------------------------------------------------------------- 1 | build = { 2 | install = { 3 | bin = { 4 | fir = "bin/fir.lua" 5 | } 6 | }, 7 | modules = { 8 | ["fir.file"] = "fir/file.lua", 9 | ["fir.generic.backend"] = "fir/generic/backend.lua", 10 | ["fir.generic.emit.markdown"] = "fir/generic/emit/markdown.lua", 11 | ["fir.generic.parser"] = "fir/generic/parser.lua", 12 | ["fir.version"] = "fir/version.lua" 13 | }, 14 | type = "builtin" 15 | } 16 | dependencies = { 17 | "lua >= 5.1", 18 | "argparse", 19 | "ansikit", 20 | "filekit" 21 | } 22 | description = { 23 | detailed = "fir extracts documentation comments from source code and builds trees out of them, so you can turn them into any other format.\n", 24 | homepage = "https://github.com/daelvn/fir", 25 | summary = "A language-agnostic documentation generator." 26 | } 27 | package = "fir" 28 | rockspec_format = "3.0" 29 | source = { 30 | tag = "v1.2.1", 31 | url = "git://github.com/daelvn/fir.git" 32 | } 33 | version = "1.2.1-1" 34 | -------------------------------------------------------------------------------- /rockspecs/fir-1.3.1-1.rockspec: -------------------------------------------------------------------------------- 1 | build = { 2 | install = { 3 | bin = { 4 | fir = "bin/fir.lua" 5 | } 6 | }, 7 | modules = { 8 | ["fir.file"] = "fir/file.lua", 9 | ["fir.generic.backend"] = "fir/generic/backend.lua", 10 | ["fir.generic.emit.markdown"] = "fir/generic/emit/markdown.lua", 11 | ["fir.generic.parser"] = "fir/generic/parser.lua", 12 | ["fir.version"] = "fir/version.lua" 13 | }, 14 | type = "builtin" 15 | } 16 | dependencies = { 17 | "lua >= 5.1", 18 | "argparse", 19 | "ansikit", 20 | "filekit" 21 | } 22 | description = { 23 | detailed = "fir extracts documentation comments from source code and builds trees out of them, so you can turn them into any other format.\n", 24 | homepage = "https://github.com/daelvn/fir", 25 | summary = "A language-agnostic documentation generator." 26 | } 27 | package = "fir" 28 | rockspec_format = "3.0" 29 | source = { 30 | tag = "v1.3.1", 31 | url = "git://github.com/daelvn/fir.git" 32 | } 33 | version = "1.3.1-1" 34 | -------------------------------------------------------------------------------- /rock.yml: -------------------------------------------------------------------------------- 1 | package: fir 2 | source: 3 | url: git://github.com/daelvn/fir.git 4 | description: 5 | summary: A language-agnostic documentation generator. 6 | detailed: > 7 | fir extracts documentation comments from source code and builds 8 | trees out of them, so you can turn them into any other format. 9 | homepage: https://github.com/daelvn/fir 10 | dependencies: 11 | - lua >= 5.1 12 | - argparse 13 | - ansikit 14 | - lpath 15 | - lrexlib-pcre2 16 | build: 17 | type: builtin 18 | modules: 19 | fir.generic.backend: fir/generic/backend.lua 20 | fir.generic.parser: fir/generic/parser.lua 21 | fir.generic.emit.markdown: fir/generic/emit/markdown.lua 22 | fir.generic.emit.tests: fir/generic/emit/tests.lua 23 | fir.file: fir/file.lua 24 | fir.version: fir/version.lua 25 | fir.error: fir/error.lua 26 | alfons.tasks.fir: alfons/tasks/fir.lua 27 | install: 28 | bin: 29 | fir: bin/fir.lua 30 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | ## General 4 | 5 | - Alfons taskfiles. 6 | 7 | ## Backend 8 | 9 | - `source` backend. 10 | - Backend that just returns all the source. 11 | 12 | - `generic+` backend (iterative). 13 | - Reads using `io.lines()`. 14 | 15 | ## Parser 16 | 17 | - ~~`generic` parser.~~ 18 | - ~~Add `@table`~~ 19 | - ~~Add `@field`~~ 20 | - ~~Add `@variable`~~ 21 | - ~~Add `@test`~~ 22 | 23 | - `generic+` backend (iterative). 24 | - Reads using an iterator and is an iterator itself. 25 | - Parses line-by-line for efficiency (can hold on to lines). 26 | 27 | - `passthrough` parser. 28 | - `parse` = `id` 29 | 30 | - `source` parser. 31 | - Haddock can lex, highlight and link source files and do static type analysis on them. This will probably never get added to Fir but I would love to see it. 32 | 33 | ## Emitters 34 | 35 | - `highlight` emitter. 36 | - Highlights source code. 37 | 38 | - `json` emitter. 39 | - Emits documentation as JSON. 40 | 41 | - `haddock` emitter. 42 | - Emits HTML styled like the modern haddock. 43 | 44 | ## Frontends 45 | 46 | - `docsify` frontend. 47 | - Included by default. -------------------------------------------------------------------------------- /rockspecs/fir-2.0.0-1.rockspec: -------------------------------------------------------------------------------- 1 | build = { 2 | install = { 3 | bin = { 4 | fir = "bin/fir.lua" 5 | } 6 | }, 7 | modules = { 8 | ["alfons.tasks.fir"] = "alfons/tasks/fir.lua", 9 | ["fir.error"] = "fir/error.lua", 10 | ["fir.file"] = "fir/file.lua", 11 | ["fir.generic.backend"] = "fir/generic/backend.lua", 12 | ["fir.generic.emit.markdown"] = "fir/generic/emit/markdown.lua", 13 | ["fir.generic.emit.tests"] = "fir/generic/emit/tests.lua", 14 | ["fir.generic.parser"] = "fir/generic/parser.lua", 15 | ["fir.version"] = "fir/version.lua" 16 | }, 17 | type = "builtin" 18 | } 19 | dependencies = { 20 | "lua >= 5.1", 21 | "argparse", 22 | "ansikit", 23 | "lpath" 24 | } 25 | description = { 26 | detailed = "fir extracts documentation comments from source code and builds trees out of them, so you can turn them into any other format.\n", 27 | homepage = "https://github.com/daelvn/fir", 28 | summary = "A language-agnostic documentation generator." 29 | } 30 | package = "fir" 31 | rockspec_format = "3.0" 32 | source = { 33 | tag = "v2.0.0", 34 | url = "git://github.com/daelvn/fir.git" 35 | } 36 | version = "2.0.0-1" 37 | -------------------------------------------------------------------------------- /rockspecs/fir-2.0.0-2.rockspec: -------------------------------------------------------------------------------- 1 | build = { 2 | install = { 3 | bin = { 4 | fir = "bin/fir.lua" 5 | } 6 | }, 7 | modules = { 8 | ["alfons.tasks.fir"] = "alfons/tasks/fir.lua", 9 | ["fir.error"] = "fir/error.lua", 10 | ["fir.file"] = "fir/file.lua", 11 | ["fir.generic.backend"] = "fir/generic/backend.lua", 12 | ["fir.generic.emit.markdown"] = "fir/generic/emit/markdown.lua", 13 | ["fir.generic.emit.tests"] = "fir/generic/emit/tests.lua", 14 | ["fir.generic.parser"] = "fir/generic/parser.lua", 15 | ["fir.version"] = "fir/version.lua" 16 | }, 17 | type = "builtin" 18 | } 19 | dependencies = { 20 | "lua >= 5.1", 21 | "argparse", 22 | "ansikit", 23 | "lpath", 24 | "lrexlib-pcre2" 25 | } 26 | description = { 27 | detailed = "fir extracts documentation comments from source code and builds trees out of them, so you can turn them into any other format.\n", 28 | homepage = "https://github.com/daelvn/fir", 29 | summary = "A language-agnostic documentation generator." 30 | } 31 | package = "fir" 32 | rockspec_format = "3.0" 33 | source = { 34 | tag = "v2.0.0", 35 | url = "git://github.com/daelvn/fir.git" 36 | } 37 | version = "2.0.0-2" 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json 2 | site_name: Fir 3 | site_url: https://daelvn.github.io/fir 4 | 5 | repo_url: https://github.com/daelvn/fir 6 | repo_name: daelvn/fir 7 | 8 | extra_css: 9 | - stylesheets/extra.css 10 | 11 | extra: 12 | social: 13 | - icon: fontawesome/brands/github 14 | link: https://github.com/daelvn 15 | 16 | # FIXME: https://github.com/squidfunk/mkdocs-material/issues/7599 17 | plugins: 18 | - search 19 | - social: 20 | cards_layout_options: 21 | background_color: "#2d6934" 22 | 23 | markdown_extensions: 24 | - toc: 25 | permalink: true 26 | permalink_leading: true 27 | slugify: !!python/object/apply:pymdownx.slugs.slugify {} 28 | - admonition 29 | - pymdownx.details 30 | - pymdownx.superfences 31 | - attr_list 32 | - md_in_html 33 | - pymdownx.tabbed: 34 | alternate_style: true 35 | 36 | theme: 37 | name: material 38 | palette: 39 | primary: custom 40 | icon: 41 | logo: material/pine-tree 42 | annotation: material/star-four-points-circle-outline 43 | features: 44 | - navigation.tabs 45 | - navigation.indexes 46 | - navigation.footer 47 | - content.tabs.link 48 | - content.code.annotate 49 | # - toc.integrate 50 | -------------------------------------------------------------------------------- /docs/generic/index.md: -------------------------------------------------------------------------------- 1 | # Generic module 2 | 3 | The generic module comes with Fir by default to provide a standard backend for parsing and emitting. By default, it can emit Markdown documentation, as well as [luassert](https://github.com/lunarmodules/luassert) tests. 4 | 5 | If you're looking for the tutorial to document your code using the default backend, it can be found [here](/fir/tutorial). 6 | 7 | ## The generic pipeline 8 | 9 | - The comments are extracted through `fir.generic.backend`. [Documentation here](/fir/generic/backend). 10 | - Then, they are parsed by `fir.generic.parser`. [Documentation here](/fir/generic/parser). 11 | - Lastly, they are passed through a language emitter (`fir.generic.emit.*`). [Documentation here](/fir/generic/emit/markdown). 12 | 13 | All the moving parts and data types are documented in their respective places. 14 | 15 | ## Integrating 16 | 17 | By creating functions that work with the data types specified in the other parts of the documentation, you're able to replace parts of the generic pipeline with your own parts. This is:want useful, for example, if you want to change the way the comments get parsed. 18 | 19 | !!! warning "About the CLI" 20 | The CLI was made to work only with the generic backend. You may have to roll your own CLI or adapt the existing one to use your own backend. 21 | -------------------------------------------------------------------------------- /fir/generic/index.md: -------------------------------------------------------------------------------- 1 | # Generic module 2 | 3 | The generic module comes with Fir by default to provide a standard backend for parsing and emitting. By default, it can emit Markdown documentation, as well as [luassert](https://github.com/lunarmodules/luassert) tests. 4 | 5 | If you're looking for the tutorial to document your code using the default backend, it can be found [here](/fir/tutorial). 6 | 7 | ## The generic pipeline 8 | 9 | - The comments are extracted through `fir.generic.backend`. [Documentation here](/fir/generic/backend). 10 | - Then, they are parsed by `fir.generic.parser`. [Documentation here](/fir/generic/parser). 11 | - Lastly, they are passed through a language emitter (`fir.generic.emit.*`). [Documentation here](/fir/generic/emit/markdown). 12 | 13 | All the moving parts and data types are documented in their respective places. 14 | 15 | ## Integrating 16 | 17 | By creating functions that work with the data types specified in the other parts of the documentation, you're able to replace parts of the generic pipeline with your own parts. This is:want useful, for example, if you want to change the way the comments get parsed. 18 | 19 | !!! warning "About the CLI" 20 | The CLI was made to work only with the generic backend. You may have to roll your own CLI or adapt the existing one to use your own backend. 21 | -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --md-primary-fg-color: #2d6934; 3 | --md-primary-fg-color--light: #8dcc94; 4 | --md-primary-fg-color--dark: #1b2e1d; 5 | } 6 | 7 | .fancy-scrollbar { 8 | --sb-track-color: var(--md-primary-fg-color--light); 9 | --sb-thumb-color: var(--md-primary-fg-color--dark); 10 | --sb-size: 3px; 11 | } 12 | 13 | .fancy-scrollbar::-webkit-scrollbar { 14 | width: 3px; 15 | scrollbar-width: thin; 16 | } 17 | 18 | .fancy-scrollbar::-webkit-scrollbar-track { 19 | background: var(--sb-track-color); 20 | border-radius: 8px; 21 | } 22 | 23 | .fancy-scrollbar::-webkit-scrollbar-thumb { 24 | background: var(--sb-thumb-color); 25 | border-radius: 8px; 26 | 27 | } 28 | 29 | @supports not selector(::-webkit-scrollbar) { 30 | .fancy-scrollbar { 31 | scrollbar-color: var(--sb-thumb-color) 32 | var(--sb-track-color); 33 | } 34 | } 35 | 36 | .md-typeset .fir-symbol { 37 | display: flex; 38 | align-items: center; 39 | font-size: 0.8em; 40 | font-family: monospace, monospace; 41 | overflow-x: auto; 42 | white-space: nowrap; 43 | & h3 { 44 | display: flex; 45 | align-items: center; 46 | margin: 0; 47 | } 48 | & span { 49 | margin: 0; 50 | margin-top: 0.4em; 51 | line-height: 1.4; 52 | } 53 | } 54 | 55 | .md-grid { 56 | max-width: 71rem; 57 | } 58 | -------------------------------------------------------------------------------- /fir/error.yue: -------------------------------------------------------------------------------- 1 | ---# fir.error #--- 2 | -- @internal 3 | -- Error reporting utilities 4 | import style from 'ansikit.style' 5 | 6 | --# main #-- 7 | 8 | --- @type Context 9 | --- All context that can be used to build a context message 10 | --:moon Context 11 | -- Context { 12 | -- source: string? 13 | -- tag: string? 14 | -- tag_type: string? 15 | -- section: string? 16 | -- tree: GenericAST.title? 17 | -- extra: [string]? 18 | -- } 19 | 20 | --- @function buildContext :: context:@@@Context@@@ 21 | --- Builds a context array out of a context object 22 | export buildContext = => 23 | lines = { 24 | (@tag and @tag_type and "in #{@tag_type}: %{magenta}#{@tag}%{reset}" or false) 25 | (@section and "in section: %{magenta}#{@section}%{reset}" or false) 26 | (@tree and "in tree: %{magenta}#{@tree}%{reset}" or false) 27 | ...(@extra or {}) 28 | } 29 | return [a for a in *lines when a] -- filter 30 | 31 | --- @function errorOut :: msg:string, context:[string] 32 | --- Prints an error and exits the program with an error code 33 | export errorOut = (msg, context) -> 34 | print style "%{red}!!! #{context.source}: #{msg}" 35 | -- XXX: Yuescript currently eliminates the function call when results are not assigned. 36 | -- `_=` is a workaround while I check if this is intended behavior. 37 | do _= [print style "%{red} #{ctx_line}" for ctx_line in *buildContext context] 38 | os.exit 1 39 | -------------------------------------------------------------------------------- /fir/file.yue: -------------------------------------------------------------------------------- 1 | -- fir.file (originally alfons.file, alfons.env) 2 | -- Gets the contents of a firfile 3 | fs = require "filekit" 4 | 5 | readMoon = (file) -> 6 | local content 7 | with fs.safeOpen file, "r" 8 | -- check that we could open correctly 9 | if .error 10 | return nil, "Could not open #{file}: #{.error}" 11 | -- read and compile 12 | import to_lua from require "moonscript.base" 13 | content, err = to_lua \read "*a" 14 | -- check that we could compile correctly 15 | unless content 16 | return nil, "Could not read or parse #{file}: #{err}" 17 | \close! 18 | -- return 19 | return content 20 | 21 | readLua = (file) -> 22 | local content 23 | with fs.safeOpen file, "r" 24 | -- check that we could open correctly 25 | if .error 26 | return nil, "Could not open #{file}: #{.error}" 27 | -- read and compile 28 | content = \read "*a" 29 | -- check that we could compile correctly 30 | unless content 31 | return nil, "Could not read #{file}: #{content}" 32 | \close! 33 | -- return 34 | return content 35 | 36 | loadEnv = (content, env) -> 37 | local fn 38 | switch _VERSION 39 | -- use loadstring on 5.1 40 | when "Lua 5.1" 41 | fn, err = loadstring content 42 | unless fn 43 | return nil, "Could not load Firfile content (5.1): #{err}" 44 | setfenv fn, env 45 | -- use load otherwise 46 | when "Lua 5.2", "Lua 5.3", "Lua 5.4" 47 | fn, err = load content, "Fir", "t", env 48 | unless fn 49 | return nil, "Could not load Firfile content (5.2+): #{err}" 50 | -- return 51 | return fn 52 | 53 | { :readMoon, :readLua, :loadEnv } -------------------------------------------------------------------------------- /docs/generic/backend.md: -------------------------------------------------------------------------------- 1 | # fir.generic.backend 2 | 3 | A generic implementation of a backend for the Fir 4 | documentation generator. 5 | 6 | This specific implementation uses a `language` module (defaults to `fir.generic.languages`) to 7 | parse comments from any file. 8 | 9 | Check out an example output of this backend [here](/fir/examples/generic-backend.html). 10 | 11 | ## API 12 | 13 | This is the API provided to work with the generic backend. 14 | 15 | | Element | Summary | 16 | |---------|---------| 17 | | **Functions** | | 18 | | [extract](#extract) | Extracts comment from a string separated by newlines. | 19 | | **Types** | | 20 | | [GenericComment](#GenericComment) | Comment returned by [`extract`](#extract). | 21 | | [Language](#Language) | Language type accepted by [`extract`](#extract). | 22 | 23 |
24 | ### extract  25 | :: input:string, language?:Language, options?:table -> comments:[[GenericComment](/fir/generic/backend#GenericComment)] 26 |
27 | 28 | 29 | Extracts comment from a string separated by newlines. 30 | 31 | - `patterns:boolean` (`false`): Whether to use patterns for the language fields and ignore string or not. 32 | - `ignore:string` (`"///"`): String used to determine when to start or stop ignoring comments. 33 | - `merge:boolean` (`true`): Whether to merge adjacent single-line comments. 34 | - `paragraphs:boolean` (`true`): Whether to split multi-line comments by empty strings (`""`). 35 | 36 |
37 | ### GenericComment  38 |
39 | 40 | Comment returned by [`extract`](#extract). 41 | 42 | 43 | === "Format" 44 | 45 | ```moon 46 | GenericComment { 47 | start :: number 48 | end :: number 49 | content :: [string] 50 | } 51 | ``` 52 | 53 | 54 |
55 | ### Language  56 |
57 | 58 | Language type accepted by [`extract`](#extract). 59 | 60 | 61 | === "Format" 62 | 63 | ```moon 64 | Language { 65 | single :: string 66 | multi :: [string] 67 | extensions :: [string] 68 | } 69 | ``` 70 | 71 | -------------------------------------------------------------------------------- /examples/testable.test.moon: -------------------------------------------------------------------------------- 1 | ---# Tests for examples.testable #--- 2 | -- This test suite was automatically generated from documentation comments, 3 | -- the tests are embedded in the code itself. Do not edit this file. 4 | assert = require 'luassert' 5 | M = require 'examples.testable' 6 | setmetatable _ENV, {['__index']: M} 7 | 8 | --///-- 9 | -- argument and tag initialization 10 | args, tags = {...}, {} 11 | for _, v in ipairs args do tags[v] = true 12 | --///-- 13 | print 'TAP version 14' 14 | print '1..2' 15 | --///-- 16 | -- counters 17 | FIR_SUCCESSES, FIR_FAILURES, FIR_PENDINGS = 0, 0, 0 18 | -- incrementing functions 19 | FIR_SINCR, FIR_FINCR, FIR_PINCR = (() -> FIR_SUCCESSES = FIR_SUCCESSES + 1), (() -> FIR_FAILURES = FIR_FAILURES + 1), (() -> FIR_PENDINGS = FIR_PENDINGS + 1) 20 | -- other functions 21 | FIR_SUCCEED = (current, count) -> print 'ok ' .. count .. ' - ' .. current 22 | FIR_FAIL = (current, count, err) -> print 'not ok ' .. count .. ' - ' .. current .. ': ' .. (string.gsub err, '[\r\n]+', '') 23 | FIR_PEND = (current, count) -> print 'not ok ' .. count .. ' - ' .. current .. ' # SKIP' 24 | FIR_RESULT = (current) -> 25 | -- marker for pending tests 26 | FIR_NEXT_PENDING = false 27 | --///-- 28 | 29 | --# General #-- 30 | -- Tests for the whole file are placed here. 31 | 32 | --# main #-- 33 | -- Tests for main. 34 | 35 | --- @test main#1 36 | --- Test for element main #1 37 | -- - **Type:** `test` 38 | --:moon Test 39 | -- 2 == id 1 40 | xpcall (() -> 41 | if FIR_NEXT_PENDING 42 | FIR_PINCR! 43 | FIR_NEXT_PENDING = false 44 | FIR_PEND 'main#1', '1' 45 | return 46 | assert.truthy 2 == id 1 47 | FIR_SUCCEED 'main#1', '1' 48 | FIR_SINCR! 49 | ), ((err) -> 50 | FIR_FAIL 'main#1', '1', err 51 | FIR_FINCR! 52 | ) 53 | 54 | array = { 1, 2, 3 } 55 | --- @test main#2 56 | --- Test for element main #2 57 | -- - **Type:** `luassert-test` (same) 58 | --:moon Test 59 | -- ((map id) array), array 60 | xpcall (() -> 61 | if FIR_NEXT_PENDING 62 | FIR_PINCR! 63 | FIR_NEXT_PENDING = false 64 | FIR_PEND 'main#2', '2' 65 | return 66 | assert.same ((map id) array), array 67 | FIR_SUCCEED 'main#2', '2' 68 | FIR_SINCR! 69 | ), ((err) -> 70 | FIR_FAIL 'main#2', '2', err 71 | FIR_FINCR! 72 | ) 73 | 74 | --# `id` 75 | 76 | --# `map` 77 | 78 | FIR_RESULT! -------------------------------------------------------------------------------- /Alfons.yue: -------------------------------------------------------------------------------- 1 | tasks: 2 | always: => load 'fir' 3 | --- @task make Install a local version 4 | --- @option make [v] Version to make 5 | make: => sh "rockbuild -m --delete #{@v}" if @v 6 | --- @task release Create and upload a release of Alfons using %{magenta}`rockbuild`%{reset}. 7 | --- @option release [v] Version to release 8 | release: => sh "rockbuild -m -t #{@v} u" if @v 9 | --- @task compile Compile all MoonScript and Yuescript 10 | compile: => 11 | sh "moonc #{file}" for file in wildcard "fir/**.moon" 12 | sh "yue bin" 13 | sh "yue fir" 14 | sh "yue alfons/tasks" 15 | --- @task clean Clean all generated Lua files 16 | clean: => 17 | show "Cleaning files" 18 | for file in wildcard "**.lua" 19 | fs.remove file 20 | --- @task docs Manage documentation 21 | --- @option docs [serve] Open a preview of the documentation 22 | --- @option docs [build] Builds a static site from the documentation 23 | docs: => 24 | logo_path = '/fir/fir-logo.png' 25 | if @generate 26 | tasks.fir 27 | generate: true 28 | writer: (_path, content) -> 29 | if _path\match 'docs/index.md' 30 | writefile _path, content 31 | \gsub '> %[!TIP%]\n> ', '!!! tip\n ' 32 | \gsub '%]%(#([a-z])(.-)%)', (initial, rest) -> "](##{initial\upper!}#{rest})" 33 | \gsub '', "![Fir Logo](#{logo_path}){ width=128 height=128 align=left }" 34 | else 35 | writefile _path, content 36 | config: 37 | name: "Fir" 38 | -- general config 39 | format: "markdown" 40 | emit: 41 | symbols_in_code: true 42 | inline_type: true 43 | tabs: 44 | use: "pymdownx" 45 | 46 | -- language (moon/yue) 47 | language: {single: "--"} 48 | 49 | -- files 50 | input: { 51 | "fir/**.yue" 52 | { "README.md", verbatim: true } 53 | { "fir/**.md", verbatim: true } 54 | } 55 | 56 | transform: (path) -> 57 | if path\match "yue$" 58 | (path\match "fir/(.+)%.yue") .. ".md" 59 | elseif path\match "README.md" 60 | "index.md" 61 | else 62 | path\match "fir/(.+)" 63 | 64 | output: "docs" 65 | 66 | ignore: { 67 | "fir/version.yue" 68 | "fir/file.yue" 69 | } 70 | if @serve 71 | sh "mkdocs serve" 72 | if @build 73 | sh "mkdocs build" 74 | -------------------------------------------------------------------------------- /docs/tutorial/alfons.md: -------------------------------------------------------------------------------- 1 | # Using from Alfons 2 | 3 | As of Fir 2, you can use Fir directly from Alfons without having to go through a shell interface or have a separate configuration file. 4 | The integration is installed automatically when you install the `fir` LuaRocks package. 5 | 6 | ## Loading 7 | 8 | It is recommended that you load external tasks in the `always` task. In rare cases (using the Alfons API) this might be disabled. 9 | 10 | === "Lua" 11 | ```lua 12 | function always() 13 | load "fir" 14 | end 15 | ``` 16 | 17 | === "MoonScript" 18 | ```moon 19 | tasks: 20 | always: => load 'fir' 21 | ``` 22 | 23 | ## Using 24 | 25 | You can use Fir from Alfons by calling `tasks.fir` once it is loaded (which should be always). The task takes the following arguments: 26 | 27 | | **Argument** | **Type** | **Default** | **Meaning** | 28 | |--|--|--|--| 29 | | `generate` | Boolean | None | Equivalent to CLI `generate` command | 30 | | `dump` | Boolean | None | Equivalent to CLI `dump` command | 31 | | `reader` | `path:string -> content:string` | Alfons `readfile` | Function that reads the content of a file | 32 | | `writer` | `path:string, content:string -> void` | Alfons `writefile` | Function that writes the content of a file | 33 | | `config` | Table | None | A configuration as described for `Fir.moon`/`Fir.lua` | 34 | 35 | If you have a Firfile that looks like the following: 36 | 37 | === "Lua" 38 | ```lua 39 | name = "Fir" 40 | format = "markdown" 41 | ``` 42 | 43 | === "MoonScript" 44 | ```moon 45 | config: 46 | name: 'Fir' 47 | format: 'markdown' 48 | ``` 49 | 50 | Then you would call Fir in Alfons as such: 51 | 52 | === "Lua" 53 | ```lua 54 | function use_fir() 55 | tasks.fir { 56 | generate = true, 57 | config = { 58 | name = "Fir", 59 | format = "markdown" 60 | } 61 | } 62 | end 63 | ``` 64 | 65 | === "MoonScript" 66 | ```moon 67 | tasks: 68 | use_fir: => 69 | tasks.fir generate: true, config: 70 | name: 'Fir' 71 | format: 'markdown' 72 | ``` 73 | 74 | ### `reader`/`writer` 75 | 76 | It is possible to configure `reader` and `writer` so that you can apply transformations to the files being read or written. For example, if you are copying over your `README.md` to an `index.md` like Fir does, you might want to change the syntax of some components like admonitions. This is how you could do that: 77 | 78 | === "Lua" 79 | ```lua 80 | function generate_docs() 81 | tasks.fir { 82 | generate = true, 83 | writer = function(path, content) 84 | if path == "README.md" then 85 | writefile(path, content:gsub('> [!TIP]\n> ', '!!! tip\n ')) 86 | else 87 | writefile(path, content) 88 | end 89 | end, 90 | config = { 91 | name = "Fir", 92 | format = "markdown" 93 | } 94 | } 95 | end 96 | ``` 97 | 98 | === "MoonScript" 99 | ```moon 100 | tasks: 101 | generate_docs: => 102 | tasks.fir generate: true, 103 | writer: (path, content) => 104 | if path == 'README.md' 105 | writefile path, content\gsub '> [!TIP]\n> ', '!!! tip\n ' 106 | else 107 | writefile path, content 108 | config: 109 | name: 'Fir' 110 | format: 'markdown' 111 | ``` 112 | -------------------------------------------------------------------------------- /docs/generic/parser.md: -------------------------------------------------------------------------------- 1 | # fir.generic.parser 2 | 3 | A parser that works with the format provided by the [generic backend](/fir/generic/backend). 4 | 5 | You can see an example of parser output [here](/fir/examples/generic-parser.html). 6 | 7 | ## Helpers 8 | 9 | Several functions that aid in the process of parsing. 10 | 11 | | Element | Summary | 12 | |---------|---------| 13 | | **Functions** | | 14 | | [determineSummaryBoundary](#determineSummaryBoundary) | Gets the boundary line where a summary ends and the description begins | 15 | | [parseDescription](#parseDescription) | Parses codeblocks, tags, headers and normal text in descriptions. | 16 | | **Types** | | 17 | | [DescriptionLine](#DescriptionLine) | A single element in a description returned by `parseDescription` | 18 | 19 |
20 | ### determineSummaryBoundary  21 | :: content:[GenericComment](/fir/generic/backend#GenericComment).content, lead:string -> boundary:number 22 |
23 | 24 | 25 | Gets the boundary line where a summary ends and the description begins 26 | 27 | - Gets the boundary line where a summary ends and the description begins 28 | 29 |
30 | ### parseDescription  31 | :: description:[string] -> description:[[DescriptionLine](/fir/generic/parser#DescriptionLine)], tags:[string] 32 |
33 | 34 | 35 | Parses codeblocks, tags, headers and normal text in descriptions. 36 | 37 | 38 | #### Notes 39 | 40 | - In a codeblock, the first character of every line is removed (for a space). 41 | 42 | #### Supported tags 43 | 44 | - `@internal` - Adds an `internal` true flag to the element. 45 | 46 |
47 | ### DescriptionLine  48 |
49 | 50 | A single element in a description returned by `parseDescription` 51 | 52 | 53 | === "Format" 54 | 55 | ```moon 56 | DescriptionLine { 57 | type :: string (text|snippet|header) 58 | content :: [string] 59 | language? :: string -- only when type is snippet 60 | title? :: string -- only when type is snippet 61 | n? :: number -- only when type is header 62 | } 63 | ``` 64 | 65 | 66 | ## API 67 | 68 | This is the API provided to work with the generic parser. 69 | 70 | | Element | Summary | 71 | |---------|---------| 72 | | **Functions** | | 73 | | [parse](#parse) | Parses a list of GenericComments into a GenericAST | 74 | | **Types** | | 75 | | [GenericAST](#GenericAST) | The AST produced by `parse`. | 76 | 77 |
78 | ### parse  79 | :: comments:[[GenericComment](/fir/generic/backend#GenericComment)], language:Language -> ast:[GenericAST](/fir/generic/parser#GenericAST) 80 |
81 | 82 | 83 | Parses a list of GenericComments into a GenericAST 84 | 85 | - Parses a list of GenericComments into a GenericAST 86 | 87 |
88 | ### GenericAST  89 |
90 | 91 | **Aliases:** `GenericSection, GenericSectionContent, GenericElement` 92 | 93 | The AST produced by `parse`. 94 | 95 | 96 | === "Format" 97 | 98 | ```moon 99 | GenericAST { 100 | title :: string 101 | description :: [DescriptionLine] 102 | [n] :: GenericSection { -- where n is the ID of the section 103 | section :: GenericSectionDetails { 104 | id :: number 105 | name :: string 106 | description :: [DescriptionLine] 107 | } 108 | content :: GenericSectionContent { 109 | [name] :: GenericElement { 110 | is :: string (type|function|constant|class) 111 | description :: [DescriptionLine] 112 | name :: [string] -- an array, meant to also contain aliases 113 | summary :: string 114 | type :: string -- only when `is` is `"function"` or `"constant"` 115 | } 116 | } 117 | } 118 | } 119 | ``` 120 | 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fir 2 2 | 3 | ![Discord](https://img.shields.io/discord/454435414044966913?style=for-the-badge&logo=discord) 4 | ![GitHub Repo stars](https://img.shields.io/github/stars/daelvn/fir?style=for-the-badge&logo=github) 5 | ![GitHub Tag](https://img.shields.io/github/v/tag/daelvn/fir?style=for-the-badge&logo=github) 6 | ![LuaRocks](https://img.shields.io/luarocks/v/daelvn/fir?style=for-the-badge&logo=lua) 7 | 8 | 9 | 10 | Fir is a language-agnostic documentation generator framework. It is fully modular, which means you can mix a comment extractor with a different comment parser and emitter (but you aren't going to go through the hassle of writing anything). A set of generic modules for generating Markdown documentation is provided, don't expect much more (unless you press me to write more). 11 | 12 |
13 | 14 | > [!TIP] 15 | > Check out the new Fir 2 update! Renewed, with symbol support, verbatim inputs, and a little more. [Changelog below](#2). 16 | 17 | ## Table of contents 18 | 19 | 20 | - [Fir 2](#fir-2) 21 | - [Table of contents](#table-of-contents) 22 | - [Install](#install) 23 | - [Compiling from source](#compiling-from-source) 24 | - [Changelog](#changelog) 25 | - [Documentation](#documentation) 26 | - [Extra features](#extra-features) 27 | - [License](#license) 28 | 29 | 30 | ## Install 31 | 32 | You can install Fir via LuaRocks: 33 | 34 | ``` 35 | $ luarocks install fir 36 | ``` 37 | 38 | ### Compiling from source 39 | 40 | If you want to compile from source, you will need to install the following dependencies from LuaRocks: 41 | - `argparse` (CLI) 42 | - `ansikit` (CLI, optional if using CLI with `--silent`) 43 | - `lpath` (Library, CLI) 44 | - `lrexlib-pcre2` (Library, CLI) 45 | - `yuescript` (Compiling) 46 | - `alfons` (Compiling) 47 | - `rockbuild` (Compiling) 48 | 49 | Then, simply run `alfons compile make -v 1.0`. To clean the project, simply `alfons clean`. 50 | 51 | ## Changelog 52 | 53 | ### 2.x 54 | 55 | - **2.0.0** (04.10.2024) 56 | 57 | Fir 2 is being released! Out of the necessity to use a more modern documentation for my other, better project, [Alfons](https://github.com/daelvn/alfons), I decided to update this project. Here's the main changes: 58 | 59 | - **Revamped test suite generator.** The `tests` format now actually works, and generates robust tests from your documentation that can check for many conditions. It's even [TAP](https://testanything.org/tap-version-14-specification.html) compliant for your CI needs! 60 | - **Symbol linking.** Fir CLI using the Generic backend will now collect all symbols and let you link to them with `@@@` syntax. 61 | - **Verbatim inputs.** Those inputs will be copied over directly to the output folder. 62 | - **Alfons integration.** Fir automatically installs a loadable [Alfons](https://github.com/daelvn/alfons) task, so you can integrate it into your Alfons workflows. Use `load 'fir'` to load it. 63 | - **Symbol summaries can now be longer than one line.** The summary ends on the first line that does not begin with a \ character. 64 | - **Aliases are deranked.** Now (under the Generic backend) aliases are not put beside the name. 65 | - **Better error reporting.** No more unparsable errors. 66 | - **Output for MkDocs.** The Generic Markdown emitter now has specific MkDocs options. 67 | - **Updated documentation.** It's easier to understand how the fuck to use this. 68 | - **Documentation now uses [Material for MkDocs](https://squidfunk.github.io/mkdocs-material).** For a more clean, updated and powerful look and feel. 69 | - **Switched to more robust libraries.** [lpath](https://github.com/starwing/lpath) for FS operations, [lrexlib on PCRE2 backend](https://github.com/rrthomas/lrexlib) for symbol syntax. 70 | - **Updated from MoonPlus to Yuescript.** MoonPlus has not been a thing for years and instead transitioned into being [Yuescript](https://yuescript.org). 71 | 72 | ## Documentation 73 | 74 | Check out the documentation [here](//daelvn.github.io/fir/). 75 | 76 | ## Extra features 77 | 78 | I kindly welcome new features that anyone wants to suggest or add! Feel free to open an issue or PR. 79 | 80 | ## License 81 | 82 | This project is [Unlicensed](LICENSE.md), and released to the public domain. Do whatever you wish with it! 83 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Fir 2 2 | 3 | ![Discord](https://img.shields.io/discord/454435414044966913?style=for-the-badge&logo=discord) 4 | ![GitHub Repo stars](https://img.shields.io/github/stars/daelvn/fir?style=for-the-badge&logo=github) 5 | ![GitHub Tag](https://img.shields.io/github/v/tag/daelvn/fir?style=for-the-badge&logo=github) 6 | ![LuaRocks](https://img.shields.io/luarocks/v/daelvn/fir?style=for-the-badge&logo=lua) 7 | 8 | ![Fir Logo](/fir/fir-logo.png){ width=128 height=128 align=left } 9 | 10 | Fir is a language-agnostic documentation generator framework. It is fully modular, which means you can mix a comment extractor with a different comment parser and emitter (but you aren't going to go through the hassle of writing anything). A set of generic modules for generating Markdown documentation is provided, don't expect much more (unless you press me to write more). 11 | 12 |
13 | 14 | !!! tip 15 | Check out the new Fir 2 update! Renewed, with symbol support, verbatim inputs, and a little more. [Changelog below](#2). 16 | 17 | ## Table of contents 18 | 19 | 20 | - [Fir 2](#Fir-2) 21 | - [Table of contents](#Table-of-contents) 22 | - [Install](#Install) 23 | - [Compiling from source](#Compiling-from-source) 24 | - [Changelog](#Changelog) 25 | - [Documentation](#Documentation) 26 | - [Extra features](#Extra-features) 27 | - [License](#License) 28 | 29 | 30 | ## Install 31 | 32 | You can install Fir via LuaRocks: 33 | 34 | ``` 35 | $ luarocks install fir 36 | ``` 37 | 38 | ### Compiling from source 39 | 40 | If you want to compile from source, you will need to install the following dependencies from LuaRocks: 41 | - `argparse` (CLI) 42 | - `ansikit` (CLI, optional if using CLI with `--silent`) 43 | - `lpath` (Library, CLI) 44 | - `lrexlib-pcre2` (Library, CLI) 45 | - `yuescript` (Compiling) 46 | - `alfons` (Compiling) 47 | - `rockbuild` (Compiling) 48 | 49 | Then, simply run `alfons compile make -v 1.0`. To clean the project, simply `alfons clean`. 50 | 51 | ## Changelog 52 | 53 | ### 2.x 54 | 55 | - **2.0.0** (04.10.2024) 56 | 57 | Fir 2 is being released! Out of the necessity to use a more modern documentation for my other, better project, [Alfons](https://github.com/daelvn/alfons), I decided to update this project. Here's the main changes: 58 | 59 | - **Revamped test suite generator.** The `tests` format now actually works, and generates robust tests from your documentation that can check for many conditions. It's even [TAP](https://testanything.org/tap-version-14-specification.html) compliant for your CI needs! 60 | - **Symbol linking.** Fir CLI using the Generic backend will now collect all symbols and let you link to them with `@@@` syntax. 61 | - **Verbatim inputs.** Those inputs will be copied over directly to the output folder. 62 | - **Alfons integration.** Fir automatically installs a loadable [Alfons](https://github.com/daelvn/alfons) task, so you can integrate it into your Alfons workflows. Use `load 'fir'` to load it. 63 | - **Symbol summaries can now be longer than one line.** The summary ends on the first line that does not begin with a \ character. 64 | - **Aliases are deranked.** Now (under the Generic backend) aliases are not put beside the name. 65 | - **Better error reporting.** No more unparsable errors. 66 | - **Output for MkDocs.** The Generic Markdown emitter now has specific MkDocs options. 67 | - **Updated documentation.** It's easier to understand how the fuck to use this. 68 | - **Documentation now uses [Material for MkDocs](https://squidfunk.github.io/mkdocs-material).** For a more clean, updated and powerful look and feel. 69 | - **Switched to more robust libraries.** [lpath](https://github.com/starwing/lpath) for FS operations, [lrexlib on PCRE2 backend](https://github.com/rrthomas/lrexlib) for symbol syntax. 70 | - **Updated from MoonPlus to Yuescript.** MoonPlus has not been a thing for years and instead transitioned into being [Yuescript](https://yuescript.org). 71 | 72 | ## Documentation 73 | 74 | Check out the documentation [here](//daelvn.github.io/fir/). 75 | 76 | ## Extra features 77 | 78 | I kindly welcome new features that anyone wants to suggest or add! Feel free to open an issue or PR. 79 | 80 | ## License 81 | 82 | This project is [Unlicensed](LICENSE.md), and released to the public domain. Do whatever you wish with it! 83 | -------------------------------------------------------------------------------- /docs/generic/emit/markdown.md: -------------------------------------------------------------------------------- 1 | # fir.generic.emit.markdown 2 | 3 | An emitter that works with a [`GenericAST`](/fir/generic/parser#GenericAST) and turns it into markdown. 4 | 5 | ## API 6 | 7 | This is the API provided to work with the generic markdown emitter. 8 | 9 | | Element | Summary | 10 | |---------|---------| 11 | | **Functions** | | 12 | | [emit](#emit) | Emits Markdown from a [`GenericAST`](/fir/generic/parser#GenericAST) | 13 | | [emitDescription](#emitDescription) | Emits Markdown from the description of an element. | 14 | | [emitSection](#emitSection) | Emits Markdown from a [`GenericSection`](/fir/generic/parser#GenericAST) | 15 | | [listAllSymbols](#listAllSymbols) | Locates all symbols in an AST | 16 | | [replaceSymbols](#replaceSymbols) | Returns a string with the symbol syntax replaced with its documentation location | 17 | 18 |
19 | ### emit  20 | :: ast:[GenericAST](/fir/generic/parser#GenericAST), options:table -> markdown:string 21 |
22 | 23 | 24 | Emits Markdown from a [`GenericAST`](/fir/generic/parser#GenericAST) 25 | 26 | - `tabs:table`: Adds tabbed syntax to the final output. 27 | - `use:boolean|"docsify"|"pymdownx"` (`false`): Outputs tab syntax, either [docsify-tabs](https://jhildenbiddle.github.io/docsify-tabs/#/) or [pymdownx.tabbed](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#syntax). 28 | - `docsify_header:string` (`"####"`): **Unused**. Headers to use for docsify-tabs. 29 | - `all:boolean` (`false`): Also emits hidden elements. 30 | - `sections:table`: Settings for sections. 31 | - `columns:table`: Settings for columns. 32 | - `[n]:table`: (where `n` can be any number or `"*"`). Specifies the column names for section `n` (with fallback). 33 | - `[1]:string` (`"Element"`) - Left column 34 | - `[2]:string` (`"Summary"`) - Right column 35 | - `types:table`: Aliases for types. 36 | - `[type]:string`: (where `type` can be any of the types supported by [GenericAST](generic/parser.md#GenericAST)). Default values include `type=Types` and `function=Functions` among others. 37 | - `symbols:{string:string}`: Map of symbols to their documentation locations 38 | - `symbols_in_code:boolean|'mkdocs'` (false): Whether to link symbols inside code. Only supports MkDocs. 39 | 40 |
41 | ### emitDescription  42 | :: desc:[[DescriptionLine](/fir/generic/parser#DescriptionLine)], options:table -> markdown:string 43 |
44 | 45 | 46 | Emits Markdown from the description of an element. 47 | 48 | - `tabs:table`: Adds tabbed syntax to the final output. 49 | - `use:boolean|"docsify"|"pymdownx"` (`false`): Outputs tab syntax, either [docsify-tabs](https://jhildenbiddle.github.io/docsify-tabs/#/) or [pymdownx.tabbed](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#syntax). 50 | - `docsify_header:string` (`"####"`): Headers to use for docsify-tabs. 51 | - `symbols:{string:string}`: Map of symbols to their documentation location 52 | - `symbols_in_code:boolean|'mkdocs'` (false): Whether to link symbols inside code. Only supports MkDocs. 53 | 54 | #### Recognized options 55 | 56 | - `headerOffset:number` (`1`): Offsets the header levels by n 57 | 58 |
59 | ### emitSection  60 | :: section:[GenericSection](/fir/generic/parser#GenericAST), options:table -> markdown:string 61 |
62 | 63 | 64 | Emits Markdown from a [`GenericSection`](/fir/generic/parser#GenericAST) 65 | 66 | 67 | 68 |
69 | ### listAllSymbols  70 | :: ast:[GenericAST](/fir/generic/parser#GenericAST) -> {[symbol: string]: location:string} 71 |
72 | 73 | 74 | Locates all symbols in an AST 75 | 76 | - `module:string`: Module string to use 77 | - `url_prefix:string`: Prefix of all documentation urls 78 | 79 |
80 | ### replaceSymbols  81 | :: str:string, symbols:{string:string} -> str:string 82 |
83 | 84 | 85 | Returns a string with the symbol syntax replaced with its documentation location 86 | 87 | `` 88 | -------------------------------------------------------------------------------- /alfons/tasks/fir.yue: -------------------------------------------------------------------------------- 1 | tasks: 2 | fir: => 3 | import 'fir.version' as FIR_VERSION 4 | prints "%{bold blue}Fir #{FIR_VERSION} (in Alfons mode)" 5 | 6 | -- set reader/writer 7 | @reader or= readfile 8 | @writer or= writefile 9 | 10 | -- arrow 11 | printArrow = (txt) -> prints "%{blue}==>%{white} #{txt}" unless @s or @silent 12 | printError = (txt) -> prints "%{red}#{txt}" unless @s or @silent 13 | printPlus = (txt) -> prints "%{green}+ #{txt}" unless @s or @silent 14 | printMini = (txt) -> prints "%{yellow}-> #{txt}" unless @s or @silent 15 | 16 | -- mkdir 17 | mkdirFor = (_path) -> 18 | _path = _path\match "(.+)/.-" 19 | sep, pStr = fsinfo.sep, "" 20 | for dir in _path\gmatch "[^" .. sep .. "]+" do 21 | pStr = pStr .. dir .. sep 22 | fs.mkdir pStr 23 | -- fromGlob reimplementation 24 | fromGlob = (pattern) -> [node for node in fs.glob pattern] 25 | -- reduce 26 | reduce = (array, callback, initial) -> 27 | accumulator = initial and initial or array[1] 28 | start = initial and 1 or 2 29 | for i = start, #array 30 | accumulator = callback accumulator, array[i] 31 | return accumulator 32 | -- flatten 33 | flatten = (arr) -> arr |> reduce ((acc, val) -> [ ...acc, ...val ]), [] 34 | -- makes a glob pattern filekit-compatible 35 | globCompat = (pattern) -> pattern\gsub "%*%*([^#{fsinfo.sep}])", "**#{fsinfo.sep}*%1" 36 | 37 | -- Generate docs 38 | if @generate or @dump 39 | printArrow "Generating docs#{@config?.name and (' for ' .. @config.name) or ''}" 40 | --- files --- 41 | -- check input 42 | unless @config.input? 43 | printError "No input specified." 44 | printError "Please set an 'input' field (table) in #{FILE}" 45 | os.exit 1 46 | -- compile ignore list 47 | ignore = flatten [fromGlob globCompat ig for ig in *@config.ignore] if @config.ignore 48 | -- iterate files 49 | cwd = path.cwd! 50 | files = {} 51 | emitted = {} 52 | for inp in *@config.input 53 | nodes = fromGlob globCompat (inp.verbatim and inp[1] or inp) 54 | for node in *nodes 55 | if fs.isfile node 56 | -- take ignores into account 57 | doIgnore = false 58 | if @config.ignore 59 | for ig in *ignore 60 | doIgnore = true if node\match ig 61 | -- add file 62 | unless doIgnore 63 | fname = (path.abs node)\match "#{cwd}#{fsinfo.sep}(.+)" 64 | if inp.verbatim 65 | emitted[fname] = @.reader fname 66 | else 67 | files[] = fname 68 | printPlus fname 69 | --- check language --- 70 | unless @config.language? 71 | printError "No language specified." 72 | printError "Please set a 'language' field (table) in your Taskfile" 73 | os.exit 1 74 | --- extract comments --- 75 | import extract from require "fir.generic.backend" 76 | extracted = {} 77 | for file in *files 78 | --printMini file 79 | extracted[file] = (@.reader file) |> extract _, @config.language, @config.backend or {} 80 | --- parse comments --- 81 | import parse from require "fir.generic.parser" 82 | --printArrow "Parsing comments" 83 | -- 84 | parsed = {} 85 | for file in *files 86 | --printMini file 87 | parsed[file] = extracted[file] |> parse _, @config.language 88 | --- dump? --- 89 | if args.dump 90 | import generate from "tableview" 91 | for file in *files 92 | print generate parsed[file] 93 | os.exit! 94 | --- emit --- 95 | unless @config.format? 96 | printError "No format specified." 97 | printError "Please set a 'format' field (string) in #{FILE}" 98 | os.exit 1 99 | -- option to collect symbols 100 | symbols = [] 101 | if @config.format == 'markdown' 102 | import listAllSymbols from require "fir.generic.emit.markdown" 103 | for file, parsed in pairs parsed 104 | this_symbols = listAllSymbols parsed, {url_prefix: @config.url_prefix} 105 | symbols = {...symbols, ...this_symbols} 106 | -- printMini "Symbols: #{(require 'inspect') symbols}" 107 | -- actual emitting 108 | import emit from require "fir.generic.emit.#{@config.format}" 109 | for file in *files 110 | --printMini file 111 | emitted[file] = parsed[file] |> emit _, {...(@config.emit or {}), :symbols} 112 | --- write --- 113 | unless @config.output? 114 | printError "No output folder specified." 115 | printError "Please set an 'output' field (string) in #{FILE}" 116 | os.exit 1 117 | -- printArrow "Writing documentation..." 118 | -- make docs dir 119 | fs.mkdir @config.output unless fs.isdir @config.output 120 | -- write 121 | for oldfile, content in pairs emitted 122 | -- transform if exists 123 | file = oldfile 124 | if @config.transform 125 | file = path @config.output, @config.transform oldfile 126 | --printMini file 127 | -- 128 | mkdirFor file 129 | @.writer file, content 130 | -------------------------------------------------------------------------------- /docs/generic/emit/tests.md: -------------------------------------------------------------------------------- 1 | # fir.generic.emit.tests 2 | 3 | An emitter that works with a [GenericAST](/generic/parser.md#GenericAST) and turns it into runnable tests. 4 | 5 | ## API 6 | 7 | This is the API provided to work with the test emitter. 8 | 9 | | Element | Summary | 10 | |---------|---------| 11 | | **Functions** | | 12 | | [emit](#emit) | Emits Lua tests from a `GenericAST` using `test` and `tagged-test` nodes. | 13 | | [emitFunctionCall](#emitFunctionCall) | Emits a function call across languages | 14 | | [emitIfStatement](#emitIfStatement) | Emits an if statement across languages | 15 | | [emitInlineFunctionDefinition](#emitInlineFunctionDefinition) | Emits an inline function declaration across languages | 16 | | [emitInternal](#emitInternal) | Emits tests, as an internal function to be used in several parts. | 17 | | [emitLocalDeclaration](#emitLocalDeclaration) | Emits a local declaration across languages | 18 | | [emitPairsForStatement](#emitPairsForStatement) | Emits a for statement across languages | 19 | | [emitParentheses](#emitParentheses) | Emits an expression in parentheses across langauges | 20 | | [emitString](#emitString) | Emits a string across languages | 21 | | [emitTableIndex](#emitTableIndex) | Emits a table index across languages | 22 | | [emitTableLiteral](#emitTableLiteral) | Emits a table literal across languages | 23 | | [emitTestHeader](#emitTestHeader) | Emits test headers. | 24 | | [emitTestWrapper](#emitTestWrapper) | Wraps a test line in counters and error protectors | 25 | 26 |
27 | ### emit  28 | :: ast:GenericAST, options:table -> code:string 29 |
30 | 31 | 32 | Emits Lua tests from a `GenericAST` using `test` and `tagged-test` nodes. 33 | 34 | - Emits Lua tests from a `GenericAST` using `test` and `tagged-test` nodes. 35 | 36 |
37 | ### emitFunctionCall  38 | :: fn:string, args:[string], options:table 39 |
40 | 41 | **Aliases:** `efn` 42 | 43 | Emits a function call across languages 44 | 45 | 46 | 47 |
48 | ### emitIfStatement  49 | :: condition:string, block:[string], options:table 50 |
51 | 52 | **Aliases:** `eif` 53 | 54 | Emits an if statement across languages 55 | 56 | - Emits an if statement across languages 57 | 58 |
59 | ### emitInlineFunctionDefinition  60 | :: args:[string], body:string, options:table 61 |
62 | 63 | **Aliases:** `eidef` 64 | 65 | Emits an inline function declaration across languages 66 | 67 | 68 | 69 |
70 | ### emitInternal  71 | :: description:[DescriptionLine], options:table, append:function, prepend:function, placement:string -> nil 72 |
73 | 74 | 75 | Emits tests, as an internal function to be used in several parts. 76 | 77 | 78 | 79 |
80 | ### emitLocalDeclaration  81 | :: name:string, lhs:string, options:table 82 |
83 | 84 | **Aliases:** `elocal` 85 | 86 | Emits a local declaration across languages 87 | 88 | 89 | 90 |
91 | ### emitPairsForStatement  92 | :: k:string, v:string, iterator:string, body:string, options:string 93 |
94 | 95 | **Aliases:** `eforp` 96 | 97 | Emits a for statement across languages 98 | 99 | - Emits a for statement across languages 100 | 101 |
102 | ### emitParentheses  103 | :: content:string 104 |
105 | 106 | **Aliases:** `eparen` 107 | 108 | Emits an expression in parentheses across langauges 109 | 110 | 111 | 112 |
113 | ### emitString  114 | :: content:string 115 |
116 | 117 | **Aliases:** `estring` 118 | 119 | Emits a string across languages 120 | 121 | 122 | 123 |
124 | ### emitTableIndex  125 | :: table:string, index:string 126 |
127 | 128 | **Aliases:** `eindex` 129 | 130 | Emits a table index across languages 131 | 132 | - Emits a table index across languages 133 | 134 |
135 | ### emitTableLiteral  136 | :: tbl:table, options:table 137 |
138 | 139 | **Aliases:** `etable` 140 | 141 | Emits a table literal across languages 142 | 143 | 144 | 145 |
146 | ### emitTestHeader  147 | :: node:table, count:number, options:table, append:function, prepend:function, placement:string -> nil 148 |
149 | 150 | 151 | Emits test headers. 152 | 153 | 154 | 155 |
156 | ### emitTestWrapper  157 | :: name:string, count:number, body:string, options:table -> code:string 158 |
159 | 160 | **Aliases:** `ewrap` 161 | 162 | Wraps a test line in counters and error protectors 163 | 164 | - Wraps a test line in counters and error protectors 165 | -------------------------------------------------------------------------------- /docs/tutorial/tests.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | This page explains how to use the `tests` Generic emitter to generate tests. The emitter generates a Lua file that is, re-parsable by Fir to generate Markdown information about the test. So, if desired, a test pipeline can be run twice, once to generate the tests, and another to generate the test documentation. 4 | 5 | A lot more functionality can be enabled if using the [`luassert`](https://github.com/lunarmodules/luassert) module. 6 | 7 | ## Configuring test emitting 8 | 9 | Below is the configuration for test emitting. Format must be set to `"emit"` for these options to apply. 10 | 11 | | **Option** | **Type** | **Default** | **Meaning** | 12 | |--|--|--|--| 13 | | `module` | String | [GenericAST](/fir/generic/parser#GenericAST).title | Name of the module to `require` | 14 | | `header` | String | None | Code or comments to append to the comment header | 15 | | `subheader` | String | `'General'` | Name of the documentation section for tests | 16 | | `trim` | Boolean | `true` | Whether to trim spaces on test lines | 17 | | `newline` | String | `\n` | Newline character | 18 | | `luassert` | Boolean | `true` | Whether to use `luassert` or not | 19 | | `print` | `'pretty'`\|`'plain'`\|`'plain-verbose'`\|`'tap'`\|`false` | `'pretty'` | Whether to print test parts and results | 20 | | `testHeaders` | Boolean | `true` | Whether to emit documentation headers for each test | 21 | | `docs` | Boolean | `true` | Whether to add comments to the tests at all | 22 | | `auto_unpack` | Boolean | `false` | Whether to unpack each symbol automatically | 23 | | `unpack` | \[String\] | None | List of symbols to unpack, making them accessible directly without prefixing with `M` | 24 | | `snippet` | String | None | Snippet of code to add before all the tests | 25 | | `all` | Boolean | None | Whether to set all tags to `true` (Execute all tests) | 26 | 27 | ### Printing modes 28 | 29 | Fir now offers different printing modes depending on your needs. 30 | 31 | - `pretty` will print pending and failed tests, as well as results, using colors. 32 | - `plain` does exactly the same with no color. 33 | - The `-verbose` variants will also print successes. 34 | - `tap` tries to output according to the [TAP protocol](https://testanything.org/tap-version-14-specification.html) 35 | - Setting it to `false` will not print anything 36 | 37 | 38 | ## Structure of a test 39 | 40 | - Header 41 | - `option.header` 42 | - Imports (`luassert` and your module) 43 | - Unpack `options.unpack` 44 | - `options.snippet` 45 | - Parse arguments (`{...}`) into a tag list (`tags`) 46 | - `options.all` setup 47 | - Insert docs description and `options.subheader` as its header 48 | - Iterate sections 49 | - Emit each test 50 | 51 | ## How unpacking works 52 | 53 | There are two kinds of unpacking: through `options.unpack` or by setting `options.auto_unpack`. 54 | 55 | ### Explicit unpacking 56 | 57 | All the symbols listed in `options.unpack` will be unpacked in the form `(local) = M['']` where `M` is your required module. 58 | 59 | ### Automatic unpacking 60 | 61 | The metatable of the environment is set so that `M` is a fallback table. This way, all functions and symbols can be accessed automatically without the need to list them in an unpack. 62 | 63 | ## Test lines 64 | 65 | ### Test configuration 66 | 67 | These configuration comments should go before the test you want to mark. 68 | 69 | Mark a test as pending: `?$ pending` 70 | 71 | ### Verbatim tests 72 | 73 | These kinds of tests are pasted into the code verbatim. 74 | 75 | === "Lua" 76 | ```lua 77 | -- ?! print(VALUE) 78 | ``` 79 | 80 | ### Verbatim setup 81 | 82 | These comments will embed code lines directly into the output, as to setup tests. 83 | 84 | === "Lua" 85 | ```lua 86 | -- !! array = {1, 2, 3} 87 | ``` 88 | 89 | ### Truthy tests 90 | 91 | These kinds of tests check for a truthy result. The contents are placed inside `assert`, or `assert.truthy` in the case of using `luassert`. 92 | 93 | 94 | === "Lua" 95 | ```lua 96 | -- ?? VALUE 97 | ``` 98 | 99 | ### Tagged tests 100 | 101 | Tagged tests are tests that are only run if a certain tag is passed to the runner file. Otherwise equivalent to [Truthy tests](#Truthy-tests) 102 | 103 | === "Lua" 104 | ```lua 105 | -- ?? tag: VALUE 106 | ``` 107 | 108 | ### `luassert` tests 109 | 110 | These tests directly leverage `luassert` to check for different conditions. The error message `err` is always optional. 111 | 112 | | **Letter** | **`luassert`** | **Meaning** | 113 | |--------|----------|---------| 114 | | `T` | `is_true(value, err)` | Check for `true` | 115 | | `t` | `truthy(value, err)` | Check for a truthy value | 116 | | `F` | `is_false(value, err)` | Check for `false` | 117 | | `f` | `falsy(value, err)` | Check for a falsy value | 118 | | `E` | `has_no.errors(fn, err)` | Check that a function has no errors | 119 | | `e` | `errors(fn, expected_err, err)` | Checks that a function errors | 120 | | `n` | `near(num, expected_num, tolerance)` | Checks that a number is within tolerance of expectation | 121 | | `u` | `unique(list, err, deep?)` | Checks that every value in a list is unique | 122 | | `:` | `matches(value, pattern, err)` | Checks that a value matches a Lua pattern | 123 | | `=` | `equals(value, expected, err)` | Checks that two values are equal (`==`) | 124 | | `~` | `same(value, expected, err)` | Checks that two values are similar through a deep compare | 125 | | `#` | type | Using the tag, checks if the passed expression is (type) | 126 | 127 | You can negate the test result by putting a caret (`^`) between the two interrogation signs. 128 | 129 | === "Lua" 130 | ```lua 131 | -- ??t truthy_value, "Not truthy" 132 | -- ?^?= value, expected, "They are, but shouldn't be equal" 133 | -- ??# string: stringy_value, "Not a string" 134 | ``` 135 | 136 | ### `luassert` tagged tests 137 | 138 | Equivalent to [`luassert` tests](#luassert-tests), but with tags as in [Tagged test](#Tagged-tests) syntax. 139 | 140 | !!! warning 141 | The type `luassert` test (`??#`) does not work with tagged tests because they use the tag as the type to test for. 142 | -------------------------------------------------------------------------------- /docs/tutorial/CLI.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | This page describes the CLI to the Fir framework. It can only use the [generic](generic/documenting.md) documentation format (but it can use different output formats). 4 | 5 | ## Installing 6 | 7 | The Fir CLI comes with the Fir framework via LuaRocks automatically. 8 | 9 | ## Configuration 10 | 11 | The most important part to maintaining the documentation of a project with Fir is the configuration. The configuration takes form as a Lua or MoonScript file and it defines the inputs, outputs, formats and other things related to documentation. 12 | 13 | The file must be named `Fir.lua` or `Fir.moon` for it to be detected automatically, but you can also use `-c` or `--config` to change the configuration file, and `--config-language` to set the language it should be loaded as (only supports `lua` and `moon`). 14 | 15 | !!! note "About MoonScript" 16 | **Note:** To use a Fir.moon configuration file, or any configuration file written in MoonScript, you need to install MoonScript via LuaRocks with the following command: `luarocks install moonscript` 17 | 18 | Here follows a complete description of all the options that the Fir CLI accepts. 19 | 20 | === "Lua" 21 | ```lua 22 | --- General config 23 | name = "Fir" -- name of your project, not mandatory 24 | --- Format 25 | -- turns into fir.generic.emit. 26 | format = "markdown" -- only markdown is included by default 27 | --- Emit options 28 | emit = { 29 | tabs = { 30 | use = false, -- Whether to use Tab syntax in Markdown or not 31 | docsify_header = "####" -- Header to use in the Docsify Tab syntax 32 | }, 33 | all = false, -- Also emit `@internal` sections 34 | columns = {}, -- Modify the TOC 35 | types = { -- TOC section titles 36 | type = "Types", 37 | function = "Functions", 38 | constant = "Constants", 39 | class = "Classes", 40 | test = "Tests", 41 | table = "Tables", 42 | field = "Fields", 43 | variable = "Variables" 44 | } 45 | } 46 | --- Backend options 47 | backend = { 48 | patterns = false, -- Whether to use patterns for the language strings 49 | ignore = "///", -- String used to determine when to start or stop ignoring comments 50 | merge = true, -- Whether to merge adjacent single-line comments 51 | paragraphs = true, -- Whether to split multi-line comments by empty strings 52 | } 53 | --- Language 54 | -- defines the way comments should be extracted 55 | -- with support for single-line and multiline comments. 56 | language = { 57 | single = "--", 58 | multi = { 59 | "--[[", 60 | "]]--" 61 | } 62 | } 63 | --- Files 64 | -- Input 65 | -- Every entry is passed through lpath.fs.glob, so you can use 66 | -- wildcards. Reference below. 67 | input = { 68 | "fir/**.yue", 69 | -- Verbatim files will be placed in the output folder as they 70 | -- are. 71 | { "fir/**.md", verbatim: true } 72 | } 73 | -- Transform 74 | -- Function that changes the filepath to be written. 75 | -- If the input is fir/generic/emit/markdown.yue, this function 76 | -- will transform it into `generic/emit/markdown.md`, for example. 77 | transform = function (path) 78 | if (path:match "yue$") then 79 | return (path:match "fir/(.+)%.yue") .. ".md" 80 | else 81 | return (path:match "fir/(.+)") 82 | end 83 | end 84 | -- Output folder 85 | output = "docs" 86 | -- Ignore 87 | -- List of files to ignore. Also supports wildcards (reference below). 88 | ignore = { 89 | "fir/version.mp", 90 | "fir/file.mp" 91 | } 92 | ``` 93 | 94 | === "MoonScript" 95 | ```moon 96 | config: 97 | --- General config 98 | name: "Fir" -- name of your project, not mandatory 99 | --- Format 100 | -- turns into fir.generic.emit. 101 | format: "markdown" -- only markdown is included by default 102 | --- Emit options 103 | emit: { 104 | tabs: { 105 | use: false, -- Whether to use Tab syntax in Markdown or not 106 | docsify_header: "####" -- Header to use in the Docsify Tab syntax 107 | }, 108 | all: false, -- Also emit `@internal` sections 109 | columns: {}, -- Modify the TOC 110 | types: { -- TOC section titles 111 | type: "Types", 112 | function: "Functions", 113 | constant: "Constants", 114 | class: "Classes", 115 | test: "Tests", 116 | table: "Tables", 117 | field: "Fields", 118 | variable: "Variables" 119 | } 120 | } 121 | --- Backend options 122 | backend: { 123 | patterns: false, -- Whether to use patterns for the language strings 124 | ignore: "///", -- String used to determine when to start or stop ignoring comments 125 | merge: true, -- Whether to merge adjacent single-line comments 126 | paragraphs: true, -- Whether to split multi-line comments by empty strings 127 | } 128 | --- Language 129 | -- defines the way comments should be extracted 130 | -- with support for single-line and multiline comments. 131 | language: 132 | single: "--" 133 | multi: { 134 | "--[[" 135 | "]]--" 136 | } 137 | } 138 | -- Verbatim files will be placed in the output folder as they 139 | -- are. 140 | --- Files 141 | -- Input 142 | -- Every entry is passed through lpath.fs.glob (reference below), 143 | -- so you can use wildcards. 144 | input: { 145 | "fir/**.yue" 146 | -- Verbatim files will be placed in the output folder as they 147 | -- are. 148 | { "fir/**.md", verbatim: true } 149 | } 150 | -- Transform 151 | -- Function that changes the filepath to be written. 152 | -- If the input is fir/generic/emit/markdown.yue, this function 153 | -- will transform it into `generic/emit/markdown.md` 154 | transform: (path) -> 155 | if path\match "yue$" 156 | (path\match "fir/(.+)%.mp") .. ".md" 157 | else 158 | path\match "fir/(.+)" 159 | -- Output folder 160 | output: "docs" 161 | -- Ignore 162 | -- List of files to ignore. Also supports wildcards (reference below). 163 | ignore: { 164 | "fir/version.mp" 165 | "fir/file.mp" 166 | } 167 | ``` 168 | 169 | ### Wildcards 170 | 171 | Wildcards work as described in the [lpath `fs.glob`](https://github.com/starwing/lpath#fsdirfsscandirfsglob) function documentation, with a modification that has been made by Fir. 172 | 173 | `**` not followed by the path separator will be turned into `**/*`. This is because `fs.glob` does not work with `**.txt` directly, but needs to have `**/*.txt`. 174 | 175 | ## Generating 176 | 177 | Then you can simply generate your docs with `fir generate` (or `fir g` for shory). This will automatically create all folders and files necessary. 178 | 179 | ## Viewing 180 | 181 | You may only want the Markdown files, but I recommend using a project like [Docsify](https://docsify.js.org/#/) or [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) to make your documentation look better as a webpage. The Markdown generic emitter has some facilities to integrate with both Docsify (like comments for the [`docsify-tabs` plugin](https://jhildenbiddle.github.io/docsify-tabs/#/)) and MkDocs. 182 | 183 | This page is currently using Material for MkDocs. 184 | -------------------------------------------------------------------------------- /fir/generic/backend.yue: -------------------------------------------------------------------------------- 1 | ---# fir.generic.backend #--- 2 | -- A generic implementation of a backend for the Fir 3 | -- documentation generator. 4 | -- 5 | -- This specific implementation uses a `language` module (defaults to `fir.generic.languages`) to 6 | -- parse comments from any file. 7 | -- 8 | -- Check out an example output of this backend [here](/fir/examples/generic-backend.html). 9 | {:find, :sub, :match} = string 10 | 11 | --# Utils #-- 12 | -- @internal 13 | -- Several utils used internally. 14 | 15 | --- @function sanitize :: input:string -> escaped:string 16 | --- Takes any string and escapes it to work with patterns. 17 | sanitize = (input) -> if "string" == type input then return input\gsub "[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%0" else return input 18 | 19 | --- @function trim :: input:string -> trimmed:string, n:number 20 | --- Trims the whitespace at each end of a string. 21 | -- Taken from [Lua-users wiki's StringTrim (`trim11`)](http://lua-users.org/wiki/StringTrim). 22 | trim = (input) -> return if n := find input, "%S" then match input, ".*%S", n else "" 23 | 24 | --- @function lines :: input:string -> lines:table 25 | --- Splits a table into lines. 26 | -- Taken from [Penlight's `stringx` (`splitlines`)](https://stevedonovan.github.io/Penlight/api/libraries/pl.stringx.html#splitlines). 27 | lines = (s) -> 28 | res, pos = {}, 1 29 | while true 30 | lep = string.find s, "[\r\n]", pos 31 | break unless lep 32 | le = sub s, lep, lep 33 | le = "\r\n" if (le == "\r") and ((sub s, lep+1, lep+1) == "\n") 34 | ln = sub s, pos, lep-1 35 | res[#res+1] = ln 36 | pos = lep + #le 37 | if pos <= #s 38 | res[#res+1] = sub s, pos 39 | return res 40 | 41 | --- @function lconcat :: la:[*], lb:[*] -> merged:[*] 42 | --- Concatenates two lists 43 | lconcat = (ta, tb) -> 44 | tc = {} 45 | for elem in *ta do tc[#tc+1] = elem 46 | for elem in *tb do tc[#tc+1] = elem 47 | return tc 48 | 49 | --# API #-- 50 | -- This is the API provided to work with the generic backend. 51 | 52 | --- @type Language 53 | --- Language type accepted by [`extract`](#extract). 54 | --:moon Format 55 | -- Language { 56 | -- single :: string 57 | -- multi :: [string] 58 | -- extensions :: [string] 59 | -- } 60 | --: 61 | 62 | --- @type GenericComment 63 | --- Comment returned by [`extract`](#extract). 64 | --:moon Format 65 | -- GenericComment { 66 | -- start :: number 67 | -- end :: number 68 | -- content :: [string] 69 | -- } 70 | --: 71 | 72 | --- @function extract :: input:string, language?:Language, options?:table -> comments:[@@@GenericComment@@@] 73 | --- Extracts comment from a string separated by newlines. 74 | --# Available options 75 | -- - `patterns:boolean` (`false`): Whether to use patterns for the language fields and ignore string or not. 76 | -- - `ignore:string` (`"///"`): String used to determine when to start or stop ignoring comments. 77 | -- - `merge:boolean` (`true`): Whether to merge adjacent single-line comments. 78 | -- - `paragraphs:boolean` (`true`): Whether to split multi-line comments by empty strings (`""`). 79 | 80 | --///-- 81 | export extract = (input="", language={}, options={}) -> 82 | -- lt:table - (l)ines (t)able, lines in the file 83 | -- comments:table - comments that were parsed 84 | -- comment:table - current comment 85 | -- start:number - line start 86 | -- end:number - line end 87 | -- type:string - type of comment (multi|single) 88 | -- content:[string] - comment strings 89 | -- tracking:table - table for variables that are getting tracked 90 | -- multi:boolean - whether we are in multiline mode 91 | -- ignore:boolean - whether comments are currently being ignored 92 | lt = lines input 93 | comments = {} 94 | comment = {content:{}} 95 | tracking = { 96 | multi: false 97 | ignore: false 98 | } 99 | -- options 100 | options.patterns = false if options.patterns == nil 101 | options.ignore = "///" if options.ignore == nil 102 | options.merge = true if options.merge == nil 103 | options.paragraphs = true if options.paragraphs == nil 104 | -- sanitize language 105 | igstring = options.patterns and options.ignore or sanitize options.ignore 106 | local single, multis, multie 107 | if options.patterns 108 | single = language.single or "" 109 | multis = language.multi?[1] 110 | multie = language.multi?[2] 111 | else 112 | single = (sanitize language.single or "") 113 | multis = (sanitize language.multi?[1] or false) 114 | multie = (sanitize language.multi?[2] or false) 115 | -- line loop 116 | -- lc:number - (l)ine (c)ount 117 | for lc=1, #lt 118 | line = lt[lc] 119 | -- (single)/// signals to start ignoring all contents 120 | if match line, "^#{single}#{igstring}" 121 | tracking.ignore = not tracking.ignore 122 | -- continue if currently ignoring 123 | elseif tracking.ignore 124 | continue 125 | -- close multiline comment 126 | elseif tracking.multi and multie and match line, "#{multie}" 127 | tracking.multi = false 128 | comment.end = lc 129 | comment.content[#comment.content] = match line, "^(.-)#{multie}" 130 | comments[#comments+1] = comment 131 | comment = content: {} 132 | -- if multiline, shove into current comment 133 | elseif tracking.multi 134 | comment.content[#comment.content] = line 135 | -- start multiline comment 136 | elseif multis and match line, "^#{multis}" 137 | tracking.multi = true 138 | comment.start = lc 139 | comment.content[#comment.content] = match line, "^#{multis}(.+)" 140 | -- add single line comment 141 | elseif single and match line, "^#{single}" 142 | comment.start, comment.end = lc, lc 143 | comment.content[1] = match line, "^#{single}(.-)$" 144 | comments[#comments+1] = comment 145 | comment = content: {} 146 | -- merge adjacent single comments 147 | if options.merge 148 | comments[0] = {type: false, start: 0, end: -1} 149 | for i=#comments, 1, -1 150 | curr = comments[i] 151 | prev = comments[i-1] 152 | continue unless curr.start == prev.end+1 153 | comments[i-1] = {content: (lconcat prev.content, curr.content), start: prev.start, end: curr.end} 154 | table.remove comments, i 155 | comments[0] = nil 156 | -- split in paragraphs 157 | lastcomments = {} 158 | if options.paragraphs 159 | -- iterate comments 160 | for comment in *comments 161 | -- iterate contents 162 | parts = {} 163 | part = {start: 1} 164 | for lc=1, #comment.content 165 | -- split by empty line 166 | if comment.content[lc] == "" 167 | part.end = lc - 1 168 | parts[#parts+1] = part 169 | part = {start: lc+1} 170 | else 171 | part[#part+1] = comment.content[lc] 172 | part.end = #comment.content 173 | parts[#parts+1] = part 174 | -- split parts into several comments 175 | --print generate parts 176 | for part in *parts 177 | lastcomments[#lastcomments+1] = {start: comment.start+part.start-1, end: comment.start+part.end-1, content: [l for l in *part]} 178 | comments = lastcomments 179 | -- return 180 | return comments 181 | --///-- 182 | 183 | --///-- 184 | -- readFile = (f) -> 185 | -- local content 186 | -- with io.open f, "r" 187 | -- content = \read "*a" 188 | -- \close! 189 | -- return content 190 | --print generate extract (readFile "fir/generic/backend.moon"), {single: "--"}, {} 191 | --///-- 192 | -------------------------------------------------------------------------------- /bin/fir.yue: -------------------------------------------------------------------------------- 1 | ---# bin/fir #--- 2 | -- The command line interface to Fir. 3 | -- 4 | -- It only works with the `generic` pipeline. If you wish to 5 | -- use your custom functions that are not in `fir.generic.*` 6 | -- you need to roll out your own script. 7 | import "argparse" 8 | -- import "filekit" as fs 9 | import "path.fs" as fs 10 | import "path" as fspath 11 | import "path.info" as fsinfo 12 | import "ansikit.style" as {:style} 13 | 14 | --///-- 15 | -- TODO Integrate this into alfons 16 | -- 17 | -- wtf did old me mean by this? 18 | --///-- 19 | local args 20 | with argparse! 21 | \name "fir" 22 | \description "Language-agnostic documentation generator" 23 | \epilog "fir v#{require 'fir.version'} - https://github.com/daelvn/fir" 24 | 25 | \help_description_margin 30 26 | 27 | -- CONFIG 28 | c = with \option "-c --config" 29 | \description "Configuration file for this project" 30 | cl = with \option "--config-language" 31 | \description "Uses an explicit language for the config file" 32 | \group "Configuration", c, cl 33 | 34 | -- DOCUMENTATION 35 | g = with \command "generate gen g" 36 | \description "Generates documentation for a project" 37 | d = with \command "dump d" 38 | \description "Dumps AST to stdout using tableview (does not generate docs)" 39 | \group "Documentation", g 40 | 41 | -- FLAGS 42 | sl = with \flag "-s --silent" 43 | \description "Does not print to stderr" 44 | \group "Flags", sl 45 | 46 | args = \parse! 47 | 48 | -- load config file 49 | FILE = do 50 | if args.c then args.c 51 | elseif args.config then args.config 52 | elseif fs.exists "Fir.lua" then "Fir.lua" 53 | elseif fs.exists "Fir.moon" then "Fir.moon" 54 | else error "No Firfile found." 55 | 56 | -- Also accept a custom language 57 | LANGUAGE = do 58 | if FILE\match "moon$" then "moon" 59 | elseif FILE\match "lua$" then "lua" 60 | elseif args.config_language then args.config_language 61 | else error "Cannot resolve format for Firfile." 62 | io.stderr\write "Using #{FILE} (#{LANGUAGE})\n" unless arg.silent 63 | 64 | -- Load string 65 | import readMoon, readLua, loadEnv from require "fir.file" 66 | content, contentErr = switch LANGUAGE 67 | when "moon" then readMoon FILE 68 | when "lua" then readLua FILE 69 | else error "Cannot resolve format '#{LANGUAGE}' for Firfile." 70 | unless content then error contentErr, 1 71 | 72 | -- Load with environment 73 | firEnv = {} 74 | fir, firErr = loadEnv content, firEnv 75 | if firErr then error "Could not load Firfile: #{firErr}" 76 | 77 | -- Run and get contents 78 | list = fir! 79 | rconfig = list and (list.config and list.config or {}) or {} 80 | project = firEnv 81 | for k, v in pairs rconfig do project[k] = v 82 | 83 | -- arrow 84 | printArrow = (txt) -> io.stderr\write style "%{blue}==>%{white} #{txt}\n" unless args.silent 85 | printError = (txt) -> io.stderr\write style "%{red}#{txt}\n" unless args.silent 86 | printPlus = (txt) -> io.stderr\write style "%{green}+ #{txt}\n" unless args.silent 87 | printMini = (txt) -> io.stderr\write style "%{yellow}-> #{txt}\n" unless args.silent 88 | 89 | -- readfile 90 | readFile = (f) -> 91 | local content 92 | with io.open f, "r" 93 | content = \read "*a" 94 | \close! 95 | return content 96 | -- writefile 97 | writeFile = (f, t) -> 98 | with io.open f, "w" 99 | \write t 100 | \close! 101 | -- mkdir 102 | mkdirFor = (path) -> 103 | path = path\match "(.+)/.-" 104 | sep, pStr = (package.config\sub 1, 1), "" 105 | for dir in path\gmatch "[^" .. sep .. "]+" do 106 | pStr = pStr .. dir .. sep 107 | fs.mkdir pStr 108 | -- fromGlob reimplementation 109 | fromGlob = (pattern) -> [node for node in fs.glob pattern] 110 | -- reduce 111 | reduce = (array, callback, initial) -> 112 | accumulator = initial and initial or array[1] 113 | start = initial and 1 or 2 114 | for i = start, #array 115 | accumulator = callback accumulator, array[i] 116 | return accumulator 117 | -- flatten 118 | flatten = (arr) -> arr |> reduce ((acc, val) -> [ ...acc, ...val ]), [] 119 | -- makes a glob pattern filekit-compatible 120 | globCompat = (pattern) -> pattern\gsub "%*%*([^#{fsinfo.sep}])", "**#{fsinfo.sep}*%1" 121 | 122 | -- Generate docs 123 | if args.generate or args.dump 124 | printArrow "Generating docs#{project.name? and (' for ' .. project.name) or ''}" 125 | --- files --- 126 | -- check input 127 | unless project.input? 128 | printError "No input specified." 129 | printError "Please set an 'input' field (table) in #{FILE}" 130 | os.exit 1 131 | -- compile ignore list 132 | ignore = flatten [fromGlob globCompat ig for ig in *project.ignore] if project.ignore 133 | -- iterate files 134 | cwd = fspath.cwd! 135 | files = {} 136 | emitted = {} 137 | for inp in *project.input 138 | nodes = fromGlob globCompat (inp.verbatim and inp[1] or inp) 139 | for node in *nodes 140 | if fs.isfile node 141 | -- take ignores into account 142 | doIgnore = false 143 | if project.ignore 144 | for ig in *ignore 145 | doIgnore = true if node\match ig 146 | -- add file 147 | unless doIgnore 148 | fname = (fspath.abs node)\match "#{cwd}#{fsinfo.sep}(.+)" 149 | if inp.verbatim 150 | emitted[fname] = readFile fname 151 | else 152 | files[] = fname 153 | printPlus fname 154 | --- check language --- 155 | unless project.language? 156 | printError "No language specified." 157 | printError "Please set a 'language' field (table) in #{FILE}" 158 | os.exit 1 159 | --- extract comments --- 160 | import extract from require "fir.generic.backend" 161 | --printArrow "Extracting comments" 162 | -- 163 | extracted = {} 164 | for file in *files 165 | --printMini file 166 | extracted[file] = (readFile file) |> extract _, project.language, project.backend or {} 167 | --- parse comments --- 168 | import parse from require "fir.generic.parser" 169 | --printArrow "Parsing comments" 170 | -- 171 | parsed = {} 172 | for file in *files 173 | --printMini file 174 | parsed[file] = extracted[file] |> parse _, project.language 175 | --- dump? --- 176 | if args.dump 177 | import generate from "tableview" 178 | for file in *files 179 | print generate parsed[file] 180 | os.exit! 181 | --- emit --- 182 | unless project.format? 183 | printError "No format specified." 184 | printError "Please set a 'format' field (string) in #{FILE}" 185 | os.exit 1 186 | -- option to collect symbols 187 | symbols = [] 188 | if project.format == 'markdown' 189 | import listAllSymbols from require "fir.generic.emit.markdown" 190 | for file, parsed in pairs parsed 191 | this_symbols = listAllSymbols parsed, {url_prefix: project.url_prefix} 192 | symbols = {...symbols, ...this_symbols} 193 | -- printMini "Symbols: #{(require 'inspect') symbols}" 194 | -- actual emitting 195 | import emit from require "fir.generic.emit.#{project.format}" 196 | for file in *files 197 | --printMini file 198 | emitted[file] = parsed[file] |> emit _, {...(project.emit or {}), :symbols} 199 | --- write --- 200 | unless project.output? 201 | printError "No output folder specified." 202 | printError "Please set an 'output' field (string) in #{FILE}" 203 | os.exit 1 204 | -- printArrow "Writing documentation..." 205 | -- make docs dir 206 | fs.mkdir project.output unless fs.isdir project.output 207 | -- write 208 | for oldfile, content in pairs emitted 209 | -- transform if exists 210 | file = oldfile 211 | if project.transform 212 | file = fspath project.output, project.transform oldfile 213 | --printMini file 214 | -- 215 | mkdirFor file 216 | writeFile file, content 217 | -------------------------------------------------------------------------------- /docs/tutorial/index.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | This page explains how to document your code to work with the generic/default parser. 4 | 5 | ## Top header 6 | 7 | A first-level header (`#`) with an according description is inserted once with the following syntax (`#...#` [?](#languages)): 8 | 9 | === "Python" 10 | ```py 11 | ### Title ### 12 | # Description 13 | ``` 14 | 15 | === "Lua/MoonScript" 16 | ```lua 17 | ---# Title #--- 18 | -- Description 19 | ``` 20 | 21 | The description can span as many lines as you wish as long as there is no empty line in the middle. If you want an empty line, just use an empty comment (this will also split the description into paragraphs). 22 | 23 | ### Hiding the document 24 | 25 | You can stop the document from being generated if you place `@internal` in the description as the first word. This will stop the page from being generated unless `all` is set to `true` in the [`emit` options](emit/markdown.md#emit). 26 | 27 | ## Section 28 | 29 | You can add a section (`##`) with the following syntax (`#...#` [?](#languages)): 30 | 31 | === "**Python**" 32 | 33 | ```py 34 | ## Title ## 35 | # Description 36 | ``` 37 | 38 | === "**Lua/MoonScript**" 39 | 40 | ```lua 41 | --# Title #-- 42 | -- Description 43 | ``` 44 | 45 | The description can span as many lines as you wish as long as there is no empty line in the middle. If you want an empty line, just use an empty comment (this will also split the description into paragraphs). 46 | 47 | Every section generates with its own table of contents, separating between Types and Functions. 48 | 49 | ### Hiding a section 50 | 51 | You can stop a section from being generated if you place `@internal` in the description as the first word. This will stop the section from being emitted unless `all` is set to `true` in the [`emit` options](generic/emit/markdown.md#emit). 52 | 53 | ## Functions 54 | 55 | You can document a function with the following syntax: 56 | 57 | === "**Python**" 58 | 59 | ```py 60 | ## @function name alias1 alias2 :: type signature 61 | ## Summary 62 | # Description 63 | ``` 64 | 65 | === "**Lua/MoonScript**" 66 | 67 | ```lua 68 | --- @function name alias1 alias2 :: type signature 69 | --- Summary 70 | -- Description 71 | ``` 72 | 73 | The aliases must be separated by spaces and are not mandatory. The type signature is mandatory for a function and will be copied verbatim. The summary will be placed in its own paragraph and will be used for the table of contents as well. 74 | 75 | The description can span as many lines as you wish as long as there is no empty line in the middle. If you want an empty line, just use an empty comment (this will also split the description into paragraphs). 76 | 77 | ### Hiding a function 78 | 79 | You can stop a function from being generated if you place `@internal` in the description as the first word. This will stop the function from being emitted unless `all` is set to `true` in the [`emit` options](generic/emit/markdown.md#emit). 80 | 81 | ## Types 82 | 83 | You can document a type with the following syntax: 84 | 85 | === "**Python**" 86 | 87 | ```py 88 | ## @type name alias1 alias2 89 | ## Summary 90 | #:type Format 91 | # Describe your type here 92 | ``` 93 | 94 | === "**Lua/MoonScript**" 95 | 96 | ```lua 97 | --- @type name alias1 alias2 98 | --- Summary 99 | --:type Format 100 | -- Describe your type here 101 | ``` 102 | 103 | It is customary to describe your type within a [code block](#code-block). Everything else is mostly the same as functions. 104 | 105 | ## Constants 106 | 107 | You can document a constant (does not need to be a constant, but...) using the following syntax: 108 | 109 | !!! note 110 | You can also use `@const` instead of `@constant`. 111 | 112 | === "**Python**" 113 | 114 | ```py 115 | ## @constant name alias1 alias2 :: type 116 | ## Summary 117 | # Description 118 | ``` 119 | 120 | === "**Lua/MoonScript**" 121 | 122 | ```lua 123 | --- @constant name alias1 alias2 :: type 124 | --- Summary 125 | -- Description 126 | ``` 127 | 128 | The aliases must be separated by spaces and are not mandatory. The type is mandatory for a constant and will be copied verbatim. The summary will be placed in its own paragraph and will be used for the table of contents as well. 129 | 130 | The description can span as many lines as you wish as long as there is no empty line in the middle. If you want an empty line, just use an empty comment (this will also split the description into paragraphs). 131 | 132 | ## Classes 133 | 134 | You can document classes with the following syntax: 135 | 136 | === "**Python**" 137 | 138 | ```py 139 | ## @class name alias1 alias2 140 | ## Summary 141 | # Description 142 | ``` 143 | 144 | === "**Lua/MoonScript**" 145 | 146 | ```lua 147 | --- @class name alias1 alias2 148 | --- Summary 149 | -- Description 150 | ``` 151 | 152 | The aliases must be separated by spaces and are not mandatory. The summary will be placed in its own paragraph and will be used for the table of contents as well. 153 | 154 | The description can span as many lines as you wish as long as there is no empty line in the middle. If you want an empty line, just use an empty comment (this will also split the description into paragraphs). 155 | 156 | ## Descriptions 157 | 158 | There are several tricks to make your descriptions look better. 159 | 160 | ### Headers 161 | 162 | You can place headers within descriptions as such. They will be automatically indented properly. 163 | 164 | === "**Python**" 165 | 166 | ```py 167 | ## Header 168 | # Description follow-up 169 | ``` 170 | 171 | === "**Lua/MoonScript**" 172 | 173 | ```lua 174 | --# Header 175 | -- Description follow-up 176 | ``` 177 | 178 | ### Code blocks 179 | 180 | You can add a code block with the following syntax: 181 | 182 | === "**Python**" 183 | 184 | ```py 185 | #:language Title 186 | # code 187 | ``` 188 | 189 | === "**Lua/MoonScript**" 190 | 191 | ```lua 192 | --:language Title 193 | -- code 194 | ``` 195 | 196 | If you need to continue a description after a codeblock, place a comment containing only `:` (no spaces) to stop the codeblock, or alternatively start a different codeblock. 197 | 198 | ## Ignoring comments 199 | 200 | You can ignore a set of comments by placing `(single)///(single)` (like `#///#` or `--///--`) at both the start or the end of the region you want to ignore, like so: 201 | 202 | === "**Python**" 203 | 204 | ```py 205 | #///# 206 | # These comments will be ignored 207 | #///# 208 | ``` 209 | 210 | === "**Lua/MoonScript**" 211 | 212 | ```lua 213 | --///-- 214 | -- These comments will be ignored 215 | --///-- 216 | ``` 217 | 218 | These comments will not be extracted at all. You can use this to document within your functions without the results actually being in the output (in fact, this is recommended). 219 | 220 | ## Linking to symbols 221 | 222 | You can use `@@@symbol@@@` outside of inline code or a code block to automatically link to a symbol in the project. Symbols are collected by taking all the names and aliases of all exported documentation. 223 | 224 | If there is a name conflict, you can prefix it with its parent module, like `@@@parent.symbol@@@`. 225 | 226 | ## Languages 227 | 228 | The language table passed to `extract` and `parse` define the comments that the Fir generic pipeline will look for. 229 | 230 | ### Lead 231 | 232 | The `` character mentioned [here](#top-header) is the *last* character from the `single` comment field. So if your language looks like `{single = "--"}`, the lead will be `"-"`. 233 | 234 | ## Examples 235 | 236 | The Fir source code is documented using Fir! Read through the source code and compare to the documentation to see how it's documented. 237 | -------------------------------------------------------------------------------- /fir/generic/emit/markdown.yue: -------------------------------------------------------------------------------- 1 | ---# fir.generic.emit.markdown #--- 2 | -- An emitter that works with a @@@GenericAST@@@ and turns it into markdown. 3 | {:rep, :find, :match} = string 4 | Pcre = require "rex_pcre2" 5 | 6 | --# Utils #-- 7 | -- @internal 8 | -- Utils to work internally. 9 | 10 | --- @function trim :: input:string -> trimmed:string, n:number 11 | --- Trims the whitespace at each end of a string. 12 | -- Taken from [Lua-users wiki's StringTrim (`trim11`)](http://lua-users.org/wiki/StringTrim). 13 | trim = (input) -> return if n := find input, "%S" then match input, ".*%S", n else "" 14 | 15 | --- @function bkpairs :: tbl:table -> Iterator 16 | --- Iterates over a table in key order. 17 | bkpairs = (t) -> 18 | a = {} 19 | table.insert a, n for n in pairs t 20 | table.sort a 21 | i = 0 22 | iter = -> 23 | i += 1 24 | if a[i] == nil 25 | return nil 26 | else 27 | return a[i], t[a[i]] 28 | return iter 29 | 30 | --# API #-- 31 | -- This is the API provided to work with the generic markdown emitter. 32 | 33 | --- @function replaceSymbols :: str:string, symbols:{string:string} -> str:string 34 | --- Returns a string with the symbol syntax replaced with its documentation location 35 | --:md Symbol syntax 36 | -- @@@@@@ 37 | --: 38 | export replaceSymbols = (str, symbols, no_backtick=false) -> Pcre.gsub str, "(`[^`]*?`)|@@@(.+?)@@@", (in_b, sym) -> 39 | in_b or ( 40 | symbols[sym] and "[#{no_backtick and "" or "`"}#{sym}#{no_backtick and "" or "`"}](#{symbols[sym]})" or "#{no_backtick and "" or "`"}#{sym}#{no_backtick and "" or "`"}" 41 | ) 42 | 43 | replaceNewlines = (str) -> str\gsub "\n", "
" 44 | newlinesIntoParagraphs = (str) -> str\gsub "\n", "\n\n" 45 | 46 | --- @function emitDescription :: desc:[@@@DescriptionLine@@@], options:table -> markdown:string 47 | --- Emits Markdown from the description of an element. 48 | --# Inherited options 49 | -- - `tabs:table`: Adds tabbed syntax to the final output. 50 | -- - `use:boolean|"docsify"|"pymdownx"` (`false`): Outputs tab syntax, either [docsify-tabs](https://jhildenbiddle.github.io/docsify-tabs/#/) or [pymdownx.tabbed](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#syntax). 51 | -- - `docsify_header:string` (`"####"`): Headers to use for docsify-tabs. 52 | -- - `symbols:{string:string}`: Map of symbols to their documentation location 53 | -- - `symbols_in_code:boolean|'mkdocs'` (false): Whether to link symbols inside code. Only supports MkDocs. 54 | --# Recognized options 55 | -- - `headerOffset:number` (`1`): Offsets the header levels by n 56 | --///-- 57 | export emitDescription = (desc, options={}) -> 58 | md = {} 59 | -- options 60 | options.headerOffset = 1 if options.headerOffset == nil 61 | -- emit 62 | for line in *desc 63 | switch line.type 64 | when "header" 65 | md[] = "" 66 | md[] = (rep "#", line.n + options.headerOffset) .. " " .. replaceSymbols line.content[1], options.symbols 67 | md[] = "" 68 | when "snippet" 69 | if options.tabs.use == 'docsify' 70 | if md[#md] == "" 71 | md[#md] = "" 72 | else 73 | md[] = "" 74 | md[] = "" 75 | md[] = "#{options.tabs.docsify_header} #{line.title}" 76 | md[] = "" 77 | elseif options.tabs.use == 'pymdownx' 78 | md[] = "" 79 | md[] = "=== \"#{line.title}\"" 80 | md[] = "" 81 | md[] = "#{options.tabs.use == 'pymdownx' and ' ' or ''}```#{line.language}" 82 | for codeline in *line.content 83 | md[] = (options.tabs.use == 'pymdownx' and ' ' or '') .. codeline 84 | md[] = "#{options.tabs.use == 'pymdownx' and ' ' or ''}```" 85 | if options.tabs.use == 'docsify' 86 | md[] = "" 87 | md[] = "" 88 | elseif options.tabs.use == 'pymdownx' 89 | md[] = "" 90 | when "text" 91 | md[] = replaceSymbols line.content[1], options.symbols 92 | -- return 93 | return table.concat md, "\n" 94 | --///-- 95 | 96 | --- @function emitSection :: section:@@@GenericSection@@@, options:table -> markdown:string 97 | --- Emits Markdown from a @@@GenericSection@@@ 98 | -- This function takes the same options than [emit](#emit) 99 | --///-- 100 | export emitSection = (section, options) -> 101 | md = {} 102 | -- failsafe option checking 103 | options.tabs ??= { use: false } 104 | options.tabs.docsify_header ??= "####" 105 | options.all ??= false 106 | options.columns ??= {} 107 | options.symbols ??= [] 108 | options.inline_type ??= false 109 | options.types ??= { 110 | type: "Types" 111 | function: "Functions" 112 | constant: "Constants" 113 | class: "Classes" 114 | test: "Tests" 115 | table: "Tables" 116 | field: "Fields" 117 | variable: "Variables" 118 | } 119 | -- exit if internal 120 | if section.section.tags.internal and not options.all 121 | return table.concat md, "\n" 122 | -- emit title and description 123 | if section.section.name 124 | md[#md+1] = "## #{section.section.name}" 125 | md[#md+1] = "" 126 | if section.section.description 127 | md[#md+1] = emitDescription section.section.description, {headerOffset: 2, tabs: options.tabs, symbols: options.symbols} 128 | -- sort content 129 | byis = {} 130 | for k, v in pairs section.contents 131 | byis[v.is] or= {} 132 | byis[v.is][k] = v 133 | -- create TOC 134 | -- Header 135 | md[#md+1] = "" 136 | if options.columns[section.id] 137 | md[#md+1] = "| **#{options.columns[section.id][1]}** | #{options.columns[section.id][2]} |" 138 | elseif options.columns["*"] 139 | md[#md+1] = "| #{options.columns['*'][1]} | #{options.columns['*'][2]} |" 140 | else 141 | md[#md+1] = "| Element | Summary |" 142 | md[#md+1] = "|---------|---------|" 143 | -- elements 144 | for ty, elems in pairs byis 145 | -- subheaders 146 | md[#md+1] = "| **#{options.types[ty]}** | |" 147 | for elemk, elemv in bkpairs elems 148 | md[#md+1] = "| [#{elemk}](##{elemk}) | #{replaceNewlines replaceSymbols elemv.summary, options.symbols} |" unless elemv.tags?.internal and not options.all 149 | md[#md+1] = "" 150 | -- print types, functions, classes and constants 151 | for ty, elems in pairs byis 152 | for elemk, elemv in bkpairs elems 153 | -- continue if internal 154 | continue if elemv.tags?.internal and not options.all 155 | has_type = (ty == 'function') or (ty == 'constant') or (ty == 'field') or (ty == 'variable') 156 | -- print 157 | md[] = "
" if options.inline_type 158 | md[] = "### #{options.inline_type and "#{elemk} " or elemk}" 159 | md[] = ":: #{replaceSymbols (trim elemv.type), options.symbols, true}" if has_type and options.inline_type 160 | md[] = "
" if options.inline_type 161 | md[] = "" 162 | md[] = "**Type:** `#{trim elemv.type}`{ .annotate } " if has_type and not options.inline_type 163 | md[] = "**Aliases:** `#{table.concat [n for n in *elemv.name[2,]], ', '}`" if #elemv.name > 1 164 | md[] = "" if (ty == "function") or (ty == "constant") or (ty == "field") or (ty == "variable") or (#elemv.name > 1) 165 | md[] = newlinesIntoParagraphs replaceSymbols elemv.summary, options.symbols 166 | md[] = "" 167 | md[] = emitDescription elemv.description, {headerOffset: 3, tabs: options.tabs, symbols: options.symbols} 168 | md[] = "" 169 | -- 170 | return table.concat md, "\n" 171 | --///-- 172 | 173 | --- @function listAllSymbols :: ast:@@@GenericAST@@@ -> {[symbol: string]: location:string} 174 | --- Locates all symbols in an AST 175 | --# Recognized options 176 | -- - `module:string`: Module string to use 177 | -- - `url_prefix:string`: Prefix of all documentation urls 178 | export listAllSymbols = (ast, options={}) -> 179 | base = options.module or ast.title 180 | prefix = (options.url_prefix or '/') .. base\gsub "%.", '/' 181 | prefix_parts = [part for part in base\gmatch "[^%.]+"] 182 | symbols = {} 183 | -- go into sections 184 | for i=1, #ast 185 | section = ast[i] 186 | for symbol_key, symbol_value in pairs section.contents 187 | for symbol_name in *symbol_value.name 188 | symbols[symbol_name] = "#{prefix}##{symbol_key}" 189 | symbols["#{prefix_parts[#prefix_parts]}.#{symbol_name}"] = "#{prefix}##{symbol_key}" 190 | return symbols 191 | 192 | --- @function emit :: ast:@@@GenericAST@@@, options:table -> markdown:string 193 | --- Emits Markdown from a @@@GenericAST@@@ 194 | --# Recognized options 195 | -- - `tabs:table`: Adds tabbed syntax to the final output. 196 | -- - `use:boolean|"docsify"|"pymdownx"` (`false`): Outputs tab syntax, either [docsify-tabs](https://jhildenbiddle.github.io/docsify-tabs/#/) or [pymdownx.tabbed](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#syntax). 197 | -- - `docsify_header:string` (`"####"`): **Unused**. Headers to use for docsify-tabs. 198 | -- - `all:boolean` (`false`): Also emits hidden elements. 199 | -- - `sections:table`: Settings for sections. 200 | -- - `columns:table`: Settings for columns. 201 | -- - `[n]:table`: (where `n` can be any number or `"*"`). Specifies the column names for section `n` (with fallback). 202 | -- - `[1]:string` (`"Element"`) - Left column 203 | -- - `[2]:string` (`"Summary"`) - Right column 204 | -- - `types:table`: Aliases for types. 205 | -- - `[type]:string`: (where `type` can be any of the types supported by [GenericAST](generic/parser.md#GenericAST)). Default values include `type=Types` and `function=Functions` among others. 206 | -- - `symbols:{string:string}`: Map of symbols to their documentation locations 207 | -- - `symbols_in_code:boolean|'mkdocs'` (false): Whether to link symbols inside code. Only supports MkDocs. 208 | --///- 209 | export emit = (ast, options={}) -> 210 | md = {} 211 | -- options 212 | options.tabs ??= { use: false } 213 | options.tabs.docsify_header ??= "####" 214 | options.all ??= false 215 | options.columns ??= {} 216 | options.symbols ??= {} 217 | options.symbols_in_code ??= false 218 | options.types ??= { 219 | type: "Types" 220 | function: "Functions" 221 | constant: "Constants" 222 | class: "Classes" 223 | test: "Tests" 224 | table: "Tables" 225 | field: "Fields" 226 | variable: "Variables" 227 | } 228 | -- exit if internal 229 | if ast.tags?.internal and not options.all 230 | return nil, "(internal)" 231 | -- emit title and description 232 | if ast.title 233 | md[#md+1] = "# #{ast.title}" 234 | md[#md+1] = "" 235 | if ast.description 236 | md[#md+1] = emitDescription ast.description, {headerOffset: 1, tabs: options.tabs, symbols: options.symbols} 237 | -- emit sections 238 | for i=1, #ast 239 | md[#md+1] = emitSection ast[i], options 240 | -- return 241 | return table.concat md, "\n" 242 | --///-- 243 | 244 | --///-- 245 | -- readFile = (f) -> 246 | -- local content 247 | -- with io.open f, "r" 248 | -- content = \read "*a" 249 | -- \close! 250 | -- return content 251 | -- import "fir.generic.backend" as {:extract} 252 | -- import "fir.generic.parser" as {:parse} 253 | 254 | -- const languageMoon = {single: "--"} 255 | -- (readFile "fir/generic/backend.mp") 256 | -- |> extract(_, languageMoon, {}) 257 | -- |> parse( _, languageMoon) 258 | -- |> emit 259 | -- |> print 260 | --///-- 261 | -------------------------------------------------------------------------------- /fir/generic/parser.yue: -------------------------------------------------------------------------------- 1 | ---# fir.generic.parser #--- 2 | -- A parser that works with the format provided by the [generic backend](/fir/generic/backend). 3 | -- 4 | -- You can see an example of parser output [here](/fir/examples/generic-parser.html). 5 | {:match, :find, :gsub, :sub} = string 6 | import errorOut from 'fir.error' 7 | 8 | --# Utils #-- 9 | -- @internal 10 | -- Several utils used internally. 11 | 12 | --- @function sanitize :: input:string -> escaped:string 13 | --- Takes any string and escapes it to work with patterns. 14 | -- @internal 15 | sanitize = (input) -> input\gsub "[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%0" if "string" == type input 16 | 17 | --- @function trim :: input:string -> trimmed:string, n:number 18 | --- Trims the whitespace at each end of a string. 19 | -- Taken from [Lua-users wiki's StringTrim (`trim11`)](http://lua-users.org/wiki/StringTrim). 20 | trim = (input) -> return if n := find input, "%S" then match input, ".*%S", n else "" 21 | 22 | --# Helpers #-- 23 | -- Several functions that aid in the process of parsing. 24 | 25 | --- @type DescriptionLine 26 | --- A single element in a description returned by `parseDescription` 27 | --:moon Format 28 | -- DescriptionLine { 29 | -- type :: string (text|snippet|header) 30 | -- content :: [string] 31 | -- language? :: string -- only when type is snippet 32 | -- title? :: string -- only when type is snippet 33 | -- n? :: number -- only when type is header 34 | -- } 35 | 36 | --- @function parseDescription :: description:[string] -> description:[@@@DescriptionLine@@@], tags:[string] 37 | --- Parses codeblocks, tags, headers and normal text in descriptions. 38 | -- Returns an array of @@@DescriptionLine@@@s and an array of tags. 39 | --# Notes 40 | -- - In a codeblock, the first character of every line is removed (for a space). 41 | --# Supported tags 42 | -- - `@internal` - Adds an `internal` true flag to the element. 43 | --///-- 44 | parseDescription = (desc) -> 45 | incode = false 46 | ndesc, code, tags = {}, {}, {} 47 | for line in *desc 48 | -- close snippet 49 | if line == ":" 50 | ndesc[] = code if incode 51 | incode = false 52 | -- add to snippet 53 | elseif incode 54 | code.content[] = sub line, 2 55 | -- tags 56 | elseif tag := match line, "^%s-@(%w+)(.-)" 57 | tag, value = match line, "^%s-@(%w+)(.-)" 58 | tags[tag] = (value == "") and true or value 59 | -- headers 60 | elseif header := match line, "^###%s+(.+)" 61 | ndesc[] = {content: {trim header}, type: "header", n: 3} 62 | elseif header := match line, "^##%s+(.+)" 63 | ndesc[] = {content: {trim header}, type: "header", n: 2} 64 | elseif header := match line, "^#%s+(.+)" 65 | ndesc[] = {content: {trim header}, type: "header", n: 1} 66 | elseif headern := match line, "^(%d)%s+(.+)" 67 | header, n = match line, "^(%d)%s+(.+)" 68 | ndesc[] = {content: {trim header}, type: "header", :n} 69 | -- tagged luassert tests (??x tag: .+) 70 | elseif match line, "%s-%?(%^?)([TtFfenu:=~])%s-(.-)%s-:%s-(.+)" -- tagged 71 | neg, mark, tag, line = match line, "%s-%?(%^?)([TtFfenu:=~])%s-(.-)%s-:%s-(.+)" 72 | ndesc[] = {content: {line}, :tag, :mark, negated: (neg != ""), type: "tagged-luassert-test"} 73 | -- luassert tests (??x .+) 74 | elseif match line, "%s-%?(%^?)([TtFfenu:=~])%s-(.+)" -- untagged 75 | neg, mark, line = match line, "%s-%?(%^?)([TtFfenu:=~])%s-(.+)" 76 | ndesc[] = {content: {line}, :mark, negated: (neg != ""), type: "luassert-test"} 77 | -- tagged tests (?? tag: .+) 78 | elseif taggedt := match line, "^%s-%?%?%s-(.-)%s-:%s-(.+)" 79 | tag, line = match line, "^%s-%?%?%s-(.-)%s-:%s-(.+)" 80 | ndesc[] = {content: {line}, :tag, type: "tagged-test"} 81 | -- test configuration (?$ .+) 82 | elseif match line, "%s-%?%$%s-(.+)" 83 | line = match line, "%s-%?%$%s-(.+)" 84 | ndesc[] = {content: {line}, type: "test-configuration"} 85 | -- tests (?? .+) 86 | elseif match line, "%s-%?%?%s-(.+)" 87 | line = match line, "%s-%?%?%s-(.+)" 88 | ndesc[] = {content: {line}, type: "test"} 89 | -- verbatim test lines (?! .+) 90 | elseif verbatimt := match line, "%s-%?!%s+(.+)" 91 | ndesc[] = {content: {verbatimt}, type: "verbatim-test"} 92 | -- verbatim test setup lines (!! .+) 93 | elseif verbatims := match line, "%s-!!%s+(.+)" 94 | ndesc[] = {content: {verbatims}, type: "verbatim-setup"} 95 | -- snippets 96 | elseif snippet := match line, "^:(%w+)%s+(.+)" 97 | -- if there was a previous snippet open, close it 98 | ndesc[] = code if incode 99 | -- init snippet 100 | language, title = match line, "^:(%w+)%s+(.+)" 101 | code = {:language, :title, content: {}, type: "snippet"} 102 | incode = true 103 | -- add line 104 | else 105 | ndesc[] = {type: "text", content: {line\match " ?(.+)"}} 106 | -- add last and return 107 | ndesc[] = code if incode 108 | return ndesc, tags 109 | --///-- 110 | 111 | --- @function determineSummaryBoundary :: content:@@@GenericComment@@@.content, lead:string -> boundary:number 112 | --- Gets the boundary line where a summary ends and the description begins 113 | export determineSummaryBoundary = (content, lead) -> 114 | for i = 1, #content 115 | line = content[i] 116 | if line\match "^#{lead}%s+" 117 | continue 118 | else 119 | return i + 1 120 | return #content 121 | 122 | --# API #-- 123 | -- This is the API provided to work with the generic parser. 124 | 125 | --- @type GenericAST GenericSection GenericSectionContent GenericElement 126 | --- The AST produced by `parse`. 127 | --:moon Format 128 | -- GenericAST { 129 | -- title :: string 130 | -- description :: [DescriptionLine] 131 | -- [n] :: GenericSection { -- where n is the ID of the section 132 | -- section :: GenericSectionDetails { 133 | -- id :: number 134 | -- name :: string 135 | -- description :: [DescriptionLine] 136 | -- } 137 | -- content :: GenericSectionContent { 138 | -- [name] :: GenericElement { 139 | -- is :: string (type|function|constant|class) 140 | -- description :: [DescriptionLine] 141 | -- name :: [string] -- an array, meant to also contain aliases 142 | -- summary :: string 143 | -- type :: string -- only when `is` is `"function"` or `"constant"` 144 | -- } 145 | -- } 146 | -- } 147 | -- } 148 | 149 | --- @function parse :: comments:[@@@GenericComment@@@], language:Language -> ast:@@@GenericAST@@@ 150 | --- Parses a list of GenericComments into a GenericAST 151 | --///-- 152 | export parse = (comments, language) -> 153 | ast = {} 154 | -- get character for special comments 155 | lead = sanitize sub language.single, -1 156 | -- keep track of variables 157 | tracking = { 158 | editing: false -- keep track of the part currently being edited, for fallbacks 159 | section: {id: 0, name: ""} -- keep track of the section being modified 160 | } 161 | context = { source: 'parser' } 162 | -- strip lead function 163 | stripLead = (str) -> match (str or ""), "^#{lead}+%s+(.+)" 164 | -- iterate comments 165 | for comment in *comments 166 | heading = comment.content[1] 167 | summary_boundary = determineSummaryBoundary comment.content, lead 168 | -- top header 169 | if title := match heading, "^#{lead}#(.+)##{lead}.+" 170 | tracking.editing = "title" 171 | ast.title = trim title 172 | ast.description, ast.tags = parseDescription [l for l in *comment.content[2,]] 173 | context = { ...context, tree: ast.title } 174 | -- section 175 | elseif section := match heading, "^#(.+)#.+" 176 | tracking.editing = "section" 177 | tracking.section = { 178 | id: tracking.section.id+1 179 | name: trim section 180 | } 181 | tracking.section.description, tracking.section.tags = parseDescription [l for l in *comment.content[2,]] 182 | ast[tracking.section.id] = {section: tracking.section, contents: {}} 183 | -- function 184 | elseif fn := match heading, "^#{lead}%s+@function%s+(.+)%s+::(.+)" 185 | fn, typ = match heading, "^#{lead}%s+@function%s+(.+)%s+::(.+)" 186 | names = [name for name in fn\gmatch "%S+"] 187 | main = names[1] 188 | if (not ast[tracking.section.id]) or (not ast[tracking.section.id].contents) 189 | errorOut "Functions must be contained within a section.", 190 | { ...context, tag_type: '@function', tag: main } 191 | ast[tracking.section.id].contents[main] = { 192 | is: "function" 193 | name: names 194 | type: typ 195 | summary: table.concat [stripLead comm for comm in *comment.content[2,summary_boundary]], '\n' 196 | } 197 | ast[tracking.section.id].contents[main].description, ast[tracking.section.id].contents[main].tags = parseDescription [l for l in *comment.content[summary_boundary,]] 198 | -- type 199 | elseif typ := match heading, "^#{lead}%s+@type%s+(.+)" 200 | names = [name for name in typ\gmatch "%S+"] 201 | main = names[1] 202 | if (not ast[tracking.section.id]) or (not ast[tracking.section.id].contents) 203 | errorOut "Types must be contained within a section.", 204 | { ...context, tag_type: '@type', tag: main } 205 | ast[tracking.section.id].contents[main] = { 206 | is: "type" 207 | name: names 208 | summary: table.concat [stripLead comm for comm in *comment.content[2,summary_boundary]], '\n' 209 | } 210 | ast[tracking.section.id].contents[main].description, ast[tracking.section.id].contents[main].tags = parseDescription [l for l in *comment.content[3,]] 211 | -- class 212 | elseif cls := match heading, "^#{lead}%s+@class%s+(.+)" 213 | names = [name for name in cls\gmatch "%S+"] 214 | main = names[1] 215 | if (not ast[tracking.section.id]) or (not ast[tracking.section.id].contents) 216 | errorOut "Classes must be contained within a section.", 217 | { ...context, tag_type: '@class', tag: main } 218 | ast[tracking.section.id].contents[main] = { 219 | is: "class" 220 | name: names 221 | summary: table.concat [stripLead comm for comm in *comment.content[2,summary_boundary]], '\n' 222 | } 223 | ast[tracking.section.id].contents[main].description, ast[tracking.section.id].contents[main].tags = parseDescription [l for l in *comment.content[3,]] 224 | -- const/constant 225 | elseif cst := match heading, "^#{lead}%s+@consta?n?t?%s+(.+)%s+::(.+)" 226 | cst, typ = match heading, "^#{lead}%s+@consta?n?t?%s+(.+)%s+::(.+)" 227 | names = [name for name in cst\gmatch "%S+"] 228 | main = names[1] 229 | if (not ast[tracking.section.id]) or (not ast[tracking.section.id].contents) 230 | errorOut "Constants must be contained within a section.", 231 | { ...context, tag_type: '@constant', tag: main } 232 | ast[tracking.section.id].contents[main] = { 233 | is: "constant" 234 | name: names 235 | type: typ 236 | summary: table.concat [stripLead comm for comm in *comment.content[2,summary_boundary]], '\n' 237 | } 238 | ast[tracking.section.id].contents[main].description, ast[tracking.section.id].contents[main].tags = parseDescription [l for l in *comment.content[3,]] 239 | -- test 240 | elseif test := match heading, "^#{lead}%s+@test%s+(.+)" 241 | names = [name for name in test\gmatch "%S+"] 242 | main = names[1] 243 | if (not ast[tracking.section.id]) or (not ast[tracking.section.id].contents) 244 | errorOut "Tests must be contained within a section.", 245 | { ...context, tag_type: '@test', tag: main } 246 | ast[tracking.section.id].contents[main] = { 247 | is: "test" 248 | name: names 249 | summary: table.concat [stripLead comm for comm in *comment.content[2,summary_boundary]], '\n' 250 | } 251 | ast[tracking.section.id].contents[main].description, ast[tracking.section.id].contents[main].tags = parseDescription [l for l in *comment.content[3,]] 252 | -- table 253 | elseif tbl := match heading, "^#{lead}%s+@table%s+(.+)" 254 | names = [name for name in tbl\gmatch "%S+"] 255 | main = names[1] 256 | if (not ast[tracking.section.id]) or (not ast[tracking.section.id].contents) 257 | errorOut "Tables must be contained within a section.", 258 | { ...context, tag_type: '@table', tag: main } 259 | ast[tracking.section.id].contents[main] = { 260 | is: "table" 261 | name: names 262 | summary: table.concat [stripLead comm for comm in *comment.content[2,summary_boundary]], '\n' 263 | } 264 | ast[tracking.section.id].contents[main].description, ast[tracking.section.id].contents[main].tags = parseDescription [l for l in *comment.content[3,]] 265 | -- field 266 | elseif field := match heading, "^#{lead}%s+@field%s+(.+)%s+::(.+)" 267 | fld, typ = match heading, "^#{lead}%s+@field%s+(.+)%s+::(.+)" 268 | names = [name for name in fld\gmatch "%S+"] 269 | main = names[1] 270 | if (not ast[tracking.section.id]) or (not ast[tracking.section.id].contents) 271 | errorOut "Fields must be contained within a section.", 272 | { ...context, tag_type: '@field', tag: main } 273 | ast[tracking.section.id].contents[main] = { 274 | is: "field" 275 | name: names 276 | type: typ 277 | summary: table.concat [stripLead comm for comm in *comment.content[2,summary_boundary]], '\n' 278 | } 279 | ast[tracking.section.id].contents[main].description, ast[tracking.section.id].contents[main].tags = parseDescription [l for l in *comment.content[3,]] 280 | -- var/variable 281 | elseif var := match heading, "^#{lead}%s+@vari?a?b?l?e?%s+(.+)%s+::(.+)" 282 | var, typ = match heading, "^#{lead}%s+@vari?a?b?l?e?%s+(.+)%s+::(.+)" 283 | names = [name for name in var\gmatch "%S+"] 284 | main = names[1] 285 | if (not ast[tracking.section.id]) or (not ast[tracking.section.id].contents) 286 | errorOut "Variables must be contained within a section.", 287 | { ...context, tag_type: '@variable', tag: main } 288 | ast[tracking.section.id].contents[main] = { 289 | is: "variable" 290 | name: names 291 | type: typ 292 | summary: table.concat [stripLead comm for comm in *comment.content[2,summary_boundary]], '\n' 293 | } 294 | ast[tracking.section.id].contents[main].description, ast[tracking.section.id].contents[main].tags = parseDescription [l for l in *comment.content[3,]] 295 | -- text 296 | else 297 | switch tracking.editing 298 | when "title" 299 | ast.description[#ast.description+1] = {type: "text", content: {""}} 300 | for l in *(parseDescription comment.content) do ast.description[#ast.description+1] = l 301 | when "section" 302 | tracking.section.description[#tracking.section.description+1] = {type: "text", content: {""}} 303 | for l in *(parseDescription comment.content) do tracking.section.description[#tracking.section.description+1] = l 304 | -- return 305 | return ast 306 | --///-- 307 | 308 | --///-- 309 | -- readFile = (f) -> 310 | -- local content 311 | -- with io.open f, "r" 312 | -- content = \read "*a" 313 | -- \close! 314 | -- return content 315 | -- import extract from require "fir.generic.backend" 316 | -- import generate from require "tableview" 317 | -- print generate parse (extract (readFile "fir/generic/backend.moon"), {single: "--"}, {}), {single: "--"} 318 | --///-- 319 | -------------------------------------------------------------------------------- /fir/generic/emit/tests.yue: -------------------------------------------------------------------------------- 1 | ---# fir.generic.emit.tests #--- 2 | -- An emitter that works with a [GenericAST](/generic/parser.md#GenericAST) and turns it into runnable tests. 3 | {:rep, :find, :match} = string 4 | import errorOut, buildContext from 'fir.error' 5 | 6 | --# Utils #-- 7 | -- @internal 8 | -- Utils to work internally. 9 | 10 | --- @function trim :: input:string -> trimmed:string, n:number 11 | --- Trims the whitespace at each end of a string. 12 | -- Taken from [Lua-users wiki's StringTrim (`trim11`)](http://lua-users.org/wiki/StringTrim). 13 | trim = (input) -> return if n := find input, "%S" then match input, ".*%S", n else "" 14 | 15 | --- @function fromMark :: mark:string, tag:string? -> func:string 16 | --- Returns a luassert function from a parser mark and maybe a tag 17 | fromMark = (mark, tag, context) -> switch mark 18 | when "T" then "is_true" 19 | when "t" then "truthy" 20 | when "F" then "is_false" 21 | when "f" then "falsy" 22 | when "E" then "has_no.errors" 23 | when "e" then "errors" 24 | when "n" then "near" 25 | when "u" then "unique" 26 | when ":" then "matches" 27 | when "=" then "equals" 28 | when "~" then "same" 29 | when "#" 30 | unless "string" == type tag 31 | errorOut "No tag specified while trying to use a luassert type assertion", 32 | buildContext context 33 | error "No tag specified while trying to use a luassert type assert" unless "string" == type tag 34 | return tag 35 | 36 | --- @constant KEYWORDS :: [string] 37 | --- Table of reserved keywords 38 | KEYWORDS = { 39 | "and", "break", "do", "else", "elseif", "end", 40 | "false", "for", "function", "goto", "if", "in", 41 | "local", "nil", "not", "or", "repeat", "return", 42 | "then", "true", "until", "while" 43 | } 44 | 45 | --- @function escapeName :: name:string -> escaped:string 46 | --- Escapes a variable name 47 | escapeName = (name) -> 48 | -- check that name is valid 49 | -- prepend underscore if starts with digit 50 | name = "_" .. name if name\match "^%d" 51 | -- avoid reserved keywords 52 | for keyword in *KEYWORDS do name = name\gsub keyword, "_%0" 53 | -- escape invalid characters 54 | escaped = name\gsub "[^a-zA-Z0-9_]", "_" 55 | return escaped 56 | 57 | --# API #-- 58 | -- This is the API provided to work with the test emitter. 59 | 60 | --- @function emitString estring :: content:string 61 | --- Emits a string across languages 62 | -- @internal 63 | emitString = estring = (content) -> "'#{content}'" 64 | 65 | --- @function emitParentheses eparen :: content:string 66 | --- Emits an expression in parentheses across langauges 67 | -- @internal 68 | emitParentheses = eparen = (content) -> "(#{content})" 69 | 70 | --- @function emitLocalDeclaration elocal :: name:string, lhs:string, options:table 71 | --- Emits a local declaration across languages 72 | -- @internal 73 | emitLocalDeclaration = elocal = (name, lhs, options) -> 74 | switch options.language 75 | when 'moon' 76 | return "#{name} = #{lhs}" 77 | when 'lua' 78 | return "local #{name} = #{lhs}" 79 | else 80 | return "local #{name} = #{lhs}" 81 | 82 | --- @function emitTableIndex eindex :: table:string, index:string 83 | --- Emits a table index across languages 84 | emitTableIndex = eindex = (tbl, index) -> "#{tbl}[#{index}]" 85 | 86 | --- @function emitFunctionCall efn :: fn:string, args:[string], options:table 87 | --- Emits a function call across languages 88 | -- @internal 89 | emitFunctionCall = efn = (fn, args=[], options) -> 90 | switch options.language 91 | when 'moon', 'yue' 92 | return "#{fn}#{(#args > 0) and (' ' .. table.concat args, ', ') or '!'}" 93 | when 'lua' 94 | return "#{fn}(#{table.concat args, ', '})" 95 | else 96 | return "#{fn}(#{table.concat args, ', '})" 97 | 98 | --- @function emitInlineFunctionDefinition eidef :: args:[string], body:string, options:table 99 | --- Emits an inline function declaration across languages 100 | -- @internal 101 | emitInlineFunctionDefinition = eidef = (args, body, options) -> 102 | switch options.language 103 | when 'moon', 'yue' 104 | return "(#{table.concat args, ', '}) -> #{body}" 105 | when 'lua' 106 | return "function(#{table.concat args, ', '}) #{body} end" 107 | else 108 | return "function(#{table.concat args, ', '}) #{body} end" 109 | 110 | --- @function emitTableLiteral etable :: tbl:table, options:table 111 | --- Emits a table literal across languages 112 | -- @internal 113 | emitTableLiteral = etable = (tbl, options) -> 114 | switch options.language 115 | when 'moon', 'yue' 116 | return '{' .. (table.concat [ "[#{rhs}]: #{lhs}" for rhs, lhs in pairs tbl ], ', ') .. '}' 117 | when 'lua' 118 | return '{' .. (table.concat [ "[#{rhs}] = #{lhs}" for rhs, lhs in pairs tbl ], ', ') .. '}' 119 | else 120 | return '{' .. (table.concat [ "[#{rhs}] = #{lhs}" for rhs, lhs in pairs tbl ], ', ') .. '}' 121 | 122 | --- @function emitIfStatement eif :: condition:string, block:[string], options:table 123 | --- Emits an if statement across languages 124 | emitIfStatement = eif = (condition, block=[], options) -> 125 | switch options.language 126 | when 'moon', 'yue' 127 | return table.concat ["if #{condition}", ...[" " .. block_line for block_line in *block]], '\n' 128 | when 'lua' 129 | return table.concat ["if #{condition} then", ...[" " .. block_line for block_line in *block], 'end'], '\n' 130 | else 131 | return table.concat ["if #{condition} then", ...[" " .. block_line for block_line in *block], 'end'], '\n' 132 | 133 | --- @function emitPairsForStatement eforp :: k:string, v:string, iterator:string, body:string, options:string 134 | --- Emits a for statement across languages 135 | emitPairsForStatement = eforp = (k, v, iterator, body, options) -> 136 | switch options.language 137 | when 'moon', 'yue' 138 | return "for #{k}, #{v} in #{iterator} do #{body}" 139 | when 'lua' 140 | return "for #{k}, #{v} in #{iterator} do #{body} end" 141 | else 142 | return "for #{k}, #{v} in #{iterator} do #{body} end" 143 | 144 | --- @function emitTestHeader :: node:table, count:number, options:table, append:function, prepend:function, placement:string -> nil 145 | --- Emits test headers. 146 | -- @internal 147 | emitTestHeader = (node, count, options, append, prepend, placement) -> 148 | unless options.testHeaders then return count + 1 149 | append "--- @test #{placement}##{count}" 150 | append "--- Test for element #{placement} ##{count}" 151 | append "-- - **Type:** `#{node.type}`#{(node.type\match 'luassert') and (' ('..(fromMark node.mark)..')') or ''}" 152 | append "-- - **Tag:** `#{node.tag}`" if node.tag 153 | append "--:#{options.language ?? 'moon'} Test" 154 | append "-- #{node.content[1]}" 155 | return count + 1 156 | 157 | --- @function emitTestWrapper ewrap :: name:string, count:number, body:string, options:table -> code:string 158 | --- Wraps a test line in counters and error protectors 159 | emitTestWrapper = ewrap = (name, count, body, options) -> 160 | return table.concat { 161 | efn "xpcall", 162 | { 163 | eparen (eidef {}, (table.concat { 164 | '', 165 | ' ' .. (eif 'FIR_NEXT_PENDING', { 166 | ' ' .. efn 'FIR_PINCR', {}, options 167 | ' ' .. 'FIR_NEXT_PENDING = false' 168 | ' ' .. efn 'FIR_PEND', {(estring name), (estring tostring count)}, options 169 | ' ' .. 'return' 170 | }, options) 171 | ' ' .. body 172 | ' ' .. efn "FIR_SUCCEED", {(estring name), (estring tostring count)}, options 173 | ' ' .. efn "FIR_SINCR", {}, options 174 | '', 175 | }, options.newline), 176 | options 177 | ), 178 | eparen (eidef {'err'}, (table.concat { 179 | '', 180 | ' ' .. efn "FIR_FAIL", {(estring name), (estring tostring count), 'err'}, options 181 | ' ' .. efn "FIR_FINCR", {}, options 182 | '', 183 | }, options.newline), 184 | options 185 | ), 186 | }, 187 | options 188 | }, options.newline 189 | 190 | --- @function emitInternal :: description:[DescriptionLine], options:table, append:function, prepend:function, placement:string -> nil 191 | --- Emits tests, as an internal function to be used in several parts. 192 | -- @internal 193 | emitInternal = (description, options, append, prepend, placement) -> 194 | ctrim = options.trim and trim or (x) -> x 195 | count = 1 196 | assert_fn = "assert#{options.luassert and '.truthy' or ''}" 197 | for node in *description 198 | switch node.type 199 | when "test-configuration" 200 | switch ctrim node.content[1] 201 | when "pending" 202 | append "FIR_NEXT_PENDING = true" 203 | when "test" 204 | count = emitTestHeader node, count, options, append, prepend, placement 205 | append ewrap "#{placement}##{count - 1}", count - 1, (efn assert_fn, 206 | { (ctrim node.content[1]) }, 207 | options), options 208 | append "" 209 | when "tagged-test" 210 | count = emitTestHeader node, count, options, append, prepend, placement 211 | append eif "tags['#{ctrim node.tag}']", 212 | { ewrap "#{placement}##{count - 1}", count - 1, (efn assert_fn, { (ctrim node.content[1]) }, options), options }, 213 | options 214 | append "" 215 | when "verbatim-test" 216 | count = emitTestHeader node, count, options, append, prepend, placement 217 | append ctrim node.content[1] 218 | append "" 219 | when "verbatim-setup" 220 | append ctrim node.content[1] 221 | when "luassert-test" 222 | count = emitTestHeader node, count, options, append, prepend, placement 223 | append ewrap "#{placement}##{count - 1}", count - 1, (efn "assert#{node.negated and '.not' or ''}.#{fromMark node.mark}", 224 | { ctrim node.content[1] }, 225 | options), options 226 | append "" 227 | when "tagged-luassert-test" 228 | count = emitTestHeader node, count, options, append, prepend, placement 229 | if node.mark == "#" -- special type case 230 | append ewrap "#{placement}##{count - 1}", count - 1, (efn "assert#{node.negated and '.not' or ''}.#{fromMark node.mark, node.tag}", 231 | { ctrim node.content[1] }, 232 | options), options 233 | else 234 | append eif (eindex 'tags', ctrim node.tag), 235 | { 236 | (ewrap "#{placement}##{count - 1}", count - 1, 237 | (efn "assert#{node.negated and '.not' or ''}.#{fromMark node.mark, node.tag}", 238 | { ctrim node.content[1] }, 239 | options), 240 | options) 241 | }, options 242 | append "" 243 | 244 | --- @function emit :: ast:GenericAST, options:table -> code:string 245 | --- Emits Lua tests from a `GenericAST` using `test` and `tagged-test` nodes. 246 | --///-- 247 | export emit = (ast, options={}) -> 248 | module = options.module or 249 | ast.title or 250 | errorOut "Could not generate tests. Module name not found. Please set %{green}`options.module`%{red}.", 251 | { source: 'emit.tests' } 252 | -- file and utils 253 | lua = { 254 | "---# Tests for #{module} #---" 255 | "-- This test suite was automatically generated from documentation comments," 256 | "-- the tests are embedded in the code itself. Do not edit this file." 257 | "#{options.header or ''}" 258 | } 259 | clength = 4 -- where to start placing lines 260 | append = (x) -> 261 | return if (not options.docs) and x\match "^%-%-" 262 | lua[] = tostring x 263 | prepend = (x, offset=0) -> 264 | return if (not options.docs) and x\match "^%-%-" 265 | table.insert lua, clength+offset, tostring x 266 | clength += 1 267 | -- options 268 | options.trim ??= true 269 | options.newline ??= "\n" 270 | options.luassert ??= true 271 | options.print ??= 'pretty' -- | 'pretty-verbose' | 'plain' | 'plain-verbose' | 'tap' | false 272 | options.testHeaders ??= true 273 | options.docs ??= true 274 | options.language ??= 'moon' 275 | options.auto_unpack ??= false 276 | -- then require 277 | if options.luassert 278 | prepend elocal 'assert', (efn 'require', { estring 'luassert' }, options), options 279 | if (options.print == 'pretty') or (options.print == 'pretty-verbose') 280 | prepend elocal 'S', (efn 'require', { estring 'ansikit.style' }, options), options 281 | prepend elocal 'M', (efn 'require', { estring module }, options), options 282 | -- now unpack methods 283 | if options.auto_unpack 284 | prepend efn 'setmetatable', {'_ENV', (etable {[estring '__index']: 'M'}, options)}, options 285 | if options.unpack 286 | prepend "" 287 | prepend "--///--" 288 | prepend "-- unpack" 289 | for name in *options.unpack 290 | prepend elocal (escapeName name), (eindex 'M', estring name), options 291 | prepend "--///--" 292 | -- add snippet 293 | if options.snippet 294 | prepend "" 295 | prepend options.snippet 296 | -- parse arguments into a tag table 297 | prepend "" 298 | prepend "--///--" 299 | prepend "-- argument and tag initialization" 300 | prepend elocal "args, tags", "{...}, {}", options 301 | -- set metatable for options.all 302 | if options.all 303 | prepend efn 'setmetatable', 304 | { 305 | 'tags', 306 | (etable { 307 | [estring '__index']: (eidef {}, 'return true', options) 308 | }, options) 309 | }, 310 | options 311 | prepend eforp '_', 'v', (efn 'ipairs', {'args'}, options), 'tags[v] = true', options 312 | prepend "--///--" 313 | -- preemptively count all tests 314 | pre_count = 0 315 | for node in *ast.description 316 | switch node.type 317 | when "test", "tagged-test", "verbatim-test", "luassert-test", "tagged-luassert-test" 318 | pre_count += 1 319 | for section in *ast 320 | for node in *section.section.description 321 | switch node.type 322 | when "test", "tagged-test", "verbatim-test", "luassert-test", "tagged-luassert-test" 323 | pre_count += 1 324 | for name, element in pairs section.contents 325 | for node in *element.description 326 | switch node.type 327 | when "test", "tagged-test", "verbatim-test", "luassert-test", "tagged-luassert-test" 328 | pre_count += 1 329 | -- if tap output, print header 330 | if options.print == 'tap' 331 | prepend efn 'print', {estring "TAP version 14"}, options 332 | prepend efn 'print', {estring "1..#{pre_count}"}, options 333 | -- setup counters and other metadata 334 | prepend "--///--" 335 | prepend "-- counters" 336 | prepend elocal "FIR_SUCCESSES, FIR_FAILURES, FIR_PENDINGS", "0, 0, 0", options 337 | prepend "-- incrementing functions" 338 | prepend elocal "FIR_SINCR, FIR_FINCR, FIR_PINCR", ( 339 | table.concat { 340 | "(" .. (eidef {}, 'FIR_SUCCESSES = FIR_SUCCESSES + 1', options) .. ")", 341 | "(" .. (eidef {}, 'FIR_FAILURES = FIR_FAILURES + 1', options) .. ")", 342 | "(" .. (eidef {}, 'FIR_PENDINGS = FIR_PENDINGS + 1', options) .. ")" 343 | }, ", " 344 | ), options 345 | prepend "-- other functions" 346 | prepend elocal "FIR_SUCCEED", (eidef {'current', 'count'}, ( 347 | switch options.print 348 | when false, 'plain', 'pretty' 349 | '' 350 | when 'tap' 351 | efn 'print', { "#{estring 'ok '} .. count .. ' - ' .. current" }, options 352 | when 'plain-verbose' 353 | efn 'print', { 354 | "#{estring "Success → "} .. current" 355 | }, options 356 | when 'pretty-verbose' 357 | efn 'print', { 358 | efn 'S.style', { 359 | "#{estring "%{green}Success%{reset} → "} .. current" 360 | }, options 361 | }, options 362 | ), options), options 363 | prepend elocal "FIR_FAIL", (eidef {'current', 'count', 'err'}, ( 364 | switch options.print 365 | when false 366 | '' 367 | when 'tap' 368 | efn 'print', { "#{estring 'not ok '} .. count .. ' - ' .. current .. ': ' .. #{eparen (efn 'string.gsub', {'err', (estring '[\\r\\n]+'), (estring '')}, options)}" }, options 369 | when 'plain', 'plain-verbose' 370 | efn 'print', { 371 | "#{estring "Failure → "} .. current .. #{estring "\\n "} .. err" 372 | }, options 373 | when 'pretty', 'pretty-verbose' 374 | efn 'print', { 375 | efn 'S.style', { 376 | "#{estring "%{red}Failure%{reset} → "} .. current .. #{estring "\\n %{white}"} .. err" 377 | }, options 378 | }, options 379 | ), options), options 380 | prepend elocal "FIR_PEND", (eidef {'current', 'count'}, ( 381 | switch options.print 382 | when false 383 | '' 384 | when 'tap' 385 | efn 'print', { "#{estring 'not ok '} .. count .. ' - ' .. current .. ' # SKIP'" }, options 386 | when 'plain', 'plain-verbose' 387 | efn 'print', { 388 | "#{estring "Pending → "} .. current" 389 | }, options 390 | when 'pretty', 'pretty-verbose' 391 | efn 'print', { 392 | efn 'S.style', { 393 | "#{estring "%{yellow}Pending%{reset} → "} .. current" 394 | }, options 395 | }, options 396 | ), options), options 397 | prepend elocal "FIR_RESULT", (eidef {'current'}, ( 398 | switch options.print 399 | when false, 'tap' 400 | '' 401 | when 'plain', 'plain-verbose' 402 | table.concat { 403 | '' 404 | ' ' .. (elocal 'total_successes', ( 405 | (efn 'string.rep', {(estring '+'), 'FIR_SUCCESSES'}, options) 406 | ), options) 407 | ' ' .. (elocal 'total_pendings', ( 408 | (efn 'string.rep', {(estring '?'), 'FIR_PENDINGS'}, options) 409 | ), options) 410 | ' ' .. (elocal 'total_failures', ( 411 | (efn 'string.rep', {(estring '-'), 'FIR_FAILURES'}, options) 412 | ), options) 413 | ' ' .. efn 'print', {}, options 414 | ' ' .. efn 'print', { "total_successes .. total_pendings .. total_failures" }, options 415 | ' ' .. efn 'print', { "#{estring '%{green}'} .. FIR_SUCCESSES .. #{estring '%{white} successes, %{yellow}'} .. FIR_PENDINGS .. #{estring '%{white} pending, %{red}'} .. FIR_FAILURES .. #{estring '%{white} errors.'}" }, options 416 | '' 417 | }, '\n' 418 | when 'pretty', 'pretty-verbose' 419 | table.concat { 420 | '' 421 | ' ' .. (elocal 'total_successes', ( 422 | efn 'S.style', { 423 | "#{estring "%{green}"} .. " .. (efn 'string.rep', {(estring '+'), 'FIR_SUCCESSES'}, options) 424 | }, options 425 | ), options) 426 | ' ' .. (elocal 'total_pendings', ( 427 | efn 'S.style', { 428 | "#{estring "%{yellow}"} .. " .. (efn 'string.rep', {(estring '?'), 'FIR_PENDINGS'}, options) 429 | }, options 430 | ), options) 431 | ' ' .. (elocal 'total_failures', ( 432 | efn 'S.style', { 433 | "#{estring "%{red}"} .. " .. (efn 'string.rep', {(estring '-'), 'FIR_FAILURES'}, options) 434 | }, options 435 | ), options) 436 | ' ' .. efn 'print', {}, options 437 | ' ' .. efn 'print', { "total_successes .. total_pendings .. total_failures" }, options 438 | ' ' .. efn 'print', { efn 'S.style', {"#{estring '%{green}'} .. FIR_SUCCESSES .. #{estring '%{white} successes, %{yellow}'} .. FIR_PENDINGS .. #{estring '%{white} pending, %{red}'} .. FIR_FAILURES .. #{estring '%{white} errors.'}" }, options }, options 439 | '' 440 | }, '\n' 441 | ), options), options 442 | prepend "-- marker for pending tests" 443 | prepend elocal "FIR_NEXT_PENDING", "false", options 444 | prepend "--///--" 445 | -- emit tests or tagged tests in description 446 | if ast.description 447 | append "--# #{options.subheader or 'General'} #--" 448 | append "-- Tests for the whole file are placed here." 449 | -- append efn 'print', {(estring "#{options.subheader or 'General'} ->")}, options if options.print 450 | append "" 451 | emitInternal ast.description, options, append, prepend, ast.title 452 | -- iterate sections 453 | for section in *ast 454 | append "--# #{section.section.name} #--" 455 | append "-- Tests for #{section.section.name}." 456 | -- append efn 'print', {(estring " #{section.section.name} ->")}, options if options.print 457 | append "" 458 | -- emit tests in description 459 | emitInternal section.section.description, options, append, prepend, section.section.name 460 | -- emit tests in every element 461 | for name, element in pairs section.contents 462 | append "--# `#{name}`" 463 | append "" 464 | emitInternal element.description, options, append, prepend, name 465 | -- 466 | append efn 'FIR_RESULT', {}, options 467 | return table.concat lua, "\n" 468 | --///-- 469 | -------------------------------------------------------------------------------- /docs/examples/generic-backend.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 76 | 77 | 78 |
79 |
80 |
81 |
82 | 83 | 84 | Table (11) 85 | 86 | 87 | table: 0x55a88bc94d00 88 | 89 | 90 |
91 |
92 |
93 | 94 |
95 | 96 | [ 97 | 98 | 99 | 1 100 | 101 | 102 | ] = 103 | 104 |
105 | 106 | Table (3) 107 | 108 | 109 | table: 0x55a88bc686d0 110 | 111 |
112 |
113 |
114 |
115 | 116 | [ 117 | 118 | 119 | "start" 120 | 121 | 122 | ] = 123 | 124 |
125 |
126 | 1 127 |
128 |
129 |
130 |
131 |
132 |
133 | 134 |
135 | 136 | [ 137 | 138 | 139 | "content" 140 | 141 | 142 | ] = 143 | 144 |
145 | 146 | Table (2) 147 | 148 | 149 | table: 0x55a88bc77ff0 150 | 151 |
152 |
153 |
154 |
155 | 156 | [ 157 | 158 | 159 | 1 160 | 161 | 162 | ] = 163 | 164 |
165 |
166 | "-# fir.generic.backend #---" 167 |
168 |
169 |
170 |
171 |
172 |
173 | 174 | [ 175 | 176 | 177 | 2 178 | 179 | 180 | ] = 181 | 182 |
183 |
184 | " A generic implementation of a [backend](#/backend.md) for the Fir documentation generator." 185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 | 195 | [ 196 | 197 | 198 | "end" 199 | 200 | 201 | ] = 202 | 203 |
204 |
205 | 2 206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 | 216 |
217 | 218 | [ 219 | 220 | 221 | 2 222 | 223 | 224 | ] = 225 | 226 |
227 | 228 | Table (3) 229 | 230 | 231 | table: 0x55a88bc78040 232 | 233 |
234 |
235 |
236 |
237 | 238 | [ 239 | 240 | 241 | "start" 242 | 243 | 244 | ] = 245 | 246 |
247 |
248 | 4 249 |
250 |
251 |
252 |
253 |
254 |
255 | 256 |
257 | 258 | [ 259 | 260 | 261 | "content" 262 | 263 | 264 | ] = 265 | 266 |
267 | 268 | Table (2) 269 | 270 | 271 | table: 0x55a88bc772b0 272 | 273 |
274 |
275 |
276 |
277 | 278 | [ 279 | 280 | 281 | 1 282 | 283 | 284 | ] = 285 | 286 |
287 |
288 | " This specific implementation uses a `language` module (defaults to `fir.generic.languages`) to" 289 |
290 |
291 |
292 |
293 |
294 |
295 | 296 | [ 297 | 298 | 299 | 2 300 | 301 | 302 | ] = 303 | 304 |
305 |
306 | " parse comments from any file." 307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 | 317 | [ 318 | 319 | 320 | "end" 321 | 322 | 323 | ] = 324 | 325 |
326 |
327 | 5 328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 | 338 |
339 | 340 | [ 341 | 342 | 343 | 3 344 | 345 | 346 | ] = 347 | 348 |
349 | 350 | Table (3) 351 | 352 | 353 | table: 0x55a88bc77330 354 | 355 |
356 |
357 |
358 |
359 | 360 | [ 361 | 362 | 363 | "start" 364 | 365 | 366 | ] = 367 | 368 |
369 |
370 | 7 371 |
372 |
373 |
374 |
375 |
376 |
377 | 378 |
379 | 380 | [ 381 | 382 | 383 | "content" 384 | 385 | 386 | ] = 387 | 388 |
389 | 390 | Table (1) 391 | 392 | 393 | table: 0x55a88baeb510 394 | 395 |
396 |
397 |
398 |
399 | 400 | [ 401 | 402 | 403 | 1 404 | 405 | 406 | ] = 407 | 408 |
409 |
410 | " This is yet another paragraph!" 411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 | 421 | [ 422 | 423 | 424 | "end" 425 | 426 | 427 | ] = 428 | 429 |
430 |
431 | 7 432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 | 442 |
443 | 444 | [ 445 | 446 | 447 | 4 448 | 449 | 450 | ] = 451 | 452 |
453 | 454 | Table (3) 455 | 456 | 457 | table: 0x55a88bb47f60 458 | 459 |
460 |
461 |
462 |
463 | 464 | [ 465 | 466 | 467 | "start" 468 | 469 | 470 | ] = 471 | 472 |
473 |
474 | 11 475 |
476 |
477 |
478 |
479 |
480 |
481 | 482 |
483 | 484 | [ 485 | 486 | 487 | "content" 488 | 489 | 490 | ] = 491 | 492 |
493 | 494 | Table (2) 495 | 496 | 497 | table: 0x55a88ba9d490 498 | 499 |
500 |
501 |
502 |
503 | 504 | [ 505 | 506 | 507 | 1 508 | 509 | 510 | ] = 511 | 512 |
513 |
514 | "# @internal Utils #--" 515 |
516 |
517 |
518 |
519 |
520 |
521 | 522 | [ 523 | 524 | 525 | 2 526 | 527 | 528 | ] = 529 | 530 |
531 |
532 | " Several utils used internally." 533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 | 543 | [ 544 | 545 | 546 | "end" 547 | 548 | 549 | ] = 550 | 551 |
552 |
553 | 12 554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 | 564 |
565 | 566 | [ 567 | 568 | 569 | 5 570 | 571 | 572 | ] = 573 | 574 |
575 | 576 | Table (3) 577 | 578 | 579 | table: 0x55a88bb66f20 580 | 581 |
582 |
583 |
584 |
585 | 586 | [ 587 | 588 | 589 | "start" 590 | 591 | 592 | ] = 593 | 594 |
595 |
596 | 14 597 |
598 |
599 |
600 |
601 |
602 |
603 | 604 |
605 | 606 | [ 607 | 608 | 609 | "content" 610 | 611 | 612 | ] = 613 | 614 |
615 | 616 | Table (2) 617 | 618 | 619 | table: 0x55a88bb448a0 620 | 621 |
622 |
623 |
624 |
625 | 626 | [ 627 | 628 | 629 | 1 630 | 631 | 632 | ] = 633 | 634 |
635 |
636 | "- @function sanitize :: input:string -> escaped:string" 637 |
638 |
639 |
640 |
641 |
642 |
643 | 644 | [ 645 | 646 | 647 | 2 648 | 649 | 650 | ] = 651 | 652 |
653 |
654 | "- Takes any string and escapes it to work with patterns." 655 |
656 |
657 |
658 |
659 |
660 |
661 |
662 |
663 |
664 | 665 | [ 666 | 667 | 668 | "end" 669 | 670 | 671 | ] = 672 | 673 |
674 |
675 | 15 676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 | 686 |
687 | 688 | [ 689 | 690 | 691 | 6 692 | 693 | 694 | ] = 695 | 696 |
697 | 698 | Table (3) 699 | 700 | 701 | table: 0x55a88bb2e170 702 | 703 |
704 |
705 |
706 |
707 | 708 | [ 709 | 710 | 711 | "start" 712 | 713 | 714 | ] = 715 | 716 |
717 |
718 | 18 719 |
720 |
721 |
722 |
723 |
724 |
725 | 726 |
727 | 728 | [ 729 | 730 | 731 | "content" 732 | 733 | 734 | ] = 735 | 736 |
737 | 738 | Table (3) 739 | 740 | 741 | table: 0x55a88bba8560 742 | 743 |
744 |
745 |
746 |
747 | 748 | [ 749 | 750 | 751 | 1 752 | 753 | 754 | ] = 755 | 756 |
757 |
758 | "- @function trim :: input:string -> trimmed:string, n:number" 759 |
760 |
761 |
762 |
763 |
764 |
765 | 766 | [ 767 | 768 | 769 | 2 770 | 771 | 772 | ] = 773 | 774 |
775 |
776 | "- Trims the whitespace at each end of a string." 777 |
778 |
779 |
780 |
781 |
782 |
783 | 784 | [ 785 | 786 | 787 | 3 788 | 789 | 790 | ] = 791 | 792 |
793 |
794 | " Taken from [Lua-users wiki's StringTrim (`trim11`)](http://lua-users.org/wiki/StringTrim)." 795 |
796 |
797 |
798 |
799 |
800 |
801 |
802 |
803 |
804 | 805 | [ 806 | 807 | 808 | "end" 809 | 810 | 811 | ] = 812 | 813 |
814 |
815 | 20 816 |
817 |
818 |
819 |
820 |
821 |
822 |
823 |
824 |
825 | 826 |
827 | 828 | [ 829 | 830 | 831 | 7 832 | 833 | 834 | ] = 835 | 836 |
837 | 838 | Table (3) 839 | 840 | 841 | table: 0x55a88bab1bf0 842 | 843 |
844 |
845 |
846 |
847 | 848 | [ 849 | 850 | 851 | "start" 852 | 853 | 854 | ] = 855 | 856 |
857 |
858 | 23 859 |
860 |
861 |
862 |
863 |
864 |
865 | 866 |
867 | 868 | [ 869 | 870 | 871 | "content" 872 | 873 | 874 | ] = 875 | 876 |
877 | 878 | Table (3) 879 | 880 | 881 | table: 0x55a88bab1cf0 882 | 883 |
884 |
885 |
886 |
887 | 888 | [ 889 | 890 | 891 | 1 892 | 893 | 894 | ] = 895 | 896 |
897 |
898 | "- @function lines = input:string -> lines:table" 899 |
900 |
901 |
902 |
903 |
904 |
905 | 906 | [ 907 | 908 | 909 | 2 910 | 911 | 912 | ] = 913 | 914 |
915 |
916 | "- Splits a table into lines." 917 |
918 |
919 |
920 |
921 |
922 |
923 | 924 | [ 925 | 926 | 927 | 3 928 | 929 | 930 | ] = 931 | 932 |
933 |
934 | " Taken from [Penlight's `stringx` (`splitlines`)](https://stevedonovan.github.io/Penlight/api/libraries/pl.stringx.html#splitlines)." 935 |
936 |
937 |
938 |
939 |
940 |
941 |
942 |
943 |
944 | 945 | [ 946 | 947 | 948 | "end" 949 | 950 | 951 | ] = 952 | 953 |
954 |
955 | 25 956 |
957 |
958 |
959 |
960 |
961 |
962 |
963 |
964 |
965 | 966 |
967 | 968 | [ 969 | 970 | 971 | 8 972 | 973 | 974 | ] = 975 | 976 |
977 | 978 | Table (3) 979 | 980 | 981 | table: 0x55a88bc08520 982 | 983 |
984 |
985 |
986 |
987 | 988 | [ 989 | 990 | 991 | "start" 992 | 993 | 994 | ] = 995 | 996 |
997 |
998 | 40 999 |
1000 |
1001 |
1002 |
1003 |
1004 |
1005 | 1006 |
1007 | 1008 | [ 1009 | 1010 | 1011 | "content" 1012 | 1013 | 1014 | ] = 1015 | 1016 |
1017 | 1018 | Table (2) 1019 | 1020 | 1021 | table: 0x55a88bc08660 1022 | 1023 |
1024 |
1025 |
1026 |
1027 | 1028 | [ 1029 | 1030 | 1031 | 1 1032 | 1033 | 1034 | ] = 1035 | 1036 |
1037 |
1038 | "- @function lconcat = la:[*], lb:[*] -> merged:[*]" 1039 |
1040 |
1041 |
1042 |
1043 |
1044 |
1045 | 1046 | [ 1047 | 1048 | 1049 | 2 1050 | 1051 | 1052 | ] = 1053 | 1054 |
1055 |
1056 | "- Concatenates two lists" 1057 |
1058 |
1059 |
1060 |
1061 |
1062 |
1063 |
1064 |
1065 |
1066 | 1067 | [ 1068 | 1069 | 1070 | "end" 1071 | 1072 | 1073 | ] = 1074 | 1075 |
1076 |
1077 | 41 1078 |
1079 |
1080 |
1081 |
1082 |
1083 |
1084 |
1085 |
1086 |
1087 | 1088 |
1089 | 1090 | [ 1091 | 1092 | 1093 | 9 1094 | 1095 | 1096 | ] = 1097 | 1098 |
1099 | 1100 | Table (3) 1101 | 1102 | 1103 | table: 0x55a88bc70e40 1104 | 1105 |
1106 |
1107 |
1108 |
1109 | 1110 | [ 1111 | 1112 | 1113 | "start" 1114 | 1115 | 1116 | ] = 1117 | 1118 |
1119 |
1120 | 48 1121 |
1122 |
1123 |
1124 |
1125 |
1126 |
1127 | 1128 |
1129 | 1130 | [ 1131 | 1132 | 1133 | "content" 1134 | 1135 | 1136 | ] = 1137 | 1138 |
1139 | 1140 | Table (2) 1141 | 1142 | 1143 | table: 0x55a88bb599e0 1144 | 1145 |
1146 |
1147 |
1148 |
1149 | 1150 | [ 1151 | 1152 | 1153 | 1 1154 | 1155 | 1156 | ] = 1157 | 1158 |
1159 |
1160 | "# API #--" 1161 |
1162 |
1163 |
1164 |
1165 |
1166 |
1167 | 1168 | [ 1169 | 1170 | 1171 | 2 1172 | 1173 | 1174 | ] = 1175 | 1176 |
1177 |
1178 | " This is the API provided to work with the generic backend." 1179 |
1180 |
1181 |
1182 |
1183 |
1184 |
1185 |
1186 |
1187 |
1188 | 1189 | [ 1190 | 1191 | 1192 | "end" 1193 | 1194 | 1195 | ] = 1196 | 1197 |
1198 |
1199 | 49 1200 |
1201 |
1202 |
1203 |
1204 |
1205 |
1206 |
1207 |
1208 |
1209 | 1210 |
1211 | 1212 | [ 1213 | 1214 | 1215 | 10 1216 | 1217 | 1218 | ] = 1219 | 1220 |
1221 | 1222 | Table (3) 1223 | 1224 | 1225 | table: 0x55a88bc03760 1226 | 1227 |
1228 |
1229 |
1230 |
1231 | 1232 | [ 1233 | 1234 | 1235 | "start" 1236 | 1237 | 1238 | ] = 1239 | 1240 |
1241 |
1242 | 51 1243 |
1244 |
1245 |
1246 |
1247 |
1248 |
1249 | 1250 |
1251 | 1252 | [ 1253 | 1254 | 1255 | "content" 1256 | 1257 | 1258 | ] = 1259 | 1260 |
1261 | 1262 | Table (9) 1263 | 1264 | 1265 | table: 0x55a88bc037f0 1266 | 1267 |
1268 |
1269 |
1270 |
1271 | 1272 | [ 1273 | 1274 | 1275 | 1 1276 | 1277 | 1278 | ] = 1279 | 1280 |
1281 |
1282 | "- @type Language" 1283 |
1284 |
1285 |
1286 |
1287 |
1288 |
1289 | 1290 | [ 1291 | 1292 | 1293 | 2 1294 | 1295 | 1296 | ] = 1297 | 1298 |
1299 |
1300 | "- Language type accepted by [`extract`](#extract)." 1301 |
1302 |
1303 |
1304 |
1305 |
1306 |
1307 | 1308 | [ 1309 | 1310 | 1311 | 3 1312 | 1313 | 1314 | ] = 1315 | 1316 |
1317 |
1318 | ":moon Format" 1319 |
1320 |
1321 |
1322 |
1323 |
1324 |
1325 | 1326 | [ 1327 | 1328 | 1329 | 4 1330 | 1331 | 1332 | ] = 1333 | 1334 |
1335 |
1336 | " {" 1337 |
1338 |
1339 |
1340 |
1341 |
1342 |
1343 | 1344 | [ 1345 | 1346 | 1347 | 5 1348 | 1349 | 1350 | ] = 1351 | 1352 |
1353 |
1354 | " single :: string" 1355 |
1356 |
1357 |
1358 |
1359 |
1360 |
1361 | 1362 | [ 1363 | 1364 | 1365 | 6 1366 | 1367 | 1368 | ] = 1369 | 1370 |
1371 |
1372 | " multi :: [string]" 1373 |
1374 |
1375 |
1376 |
1377 |
1378 |
1379 | 1380 | [ 1381 | 1382 | 1383 | 7 1384 | 1385 | 1386 | ] = 1387 | 1388 |
1389 |
1390 | " extensions :: [string]" 1391 |
1392 |
1393 |
1394 |
1395 |
1396 |
1397 | 1398 | [ 1399 | 1400 | 1401 | 8 1402 | 1403 | 1404 | ] = 1405 | 1406 |
1407 |
1408 | " }" 1409 |
1410 |
1411 |
1412 |
1413 |
1414 |
1415 | 1416 | [ 1417 | 1418 | 1419 | 9 1420 | 1421 | 1422 | ] = 1423 | 1424 |
1425 |
1426 | ":" 1427 |
1428 |
1429 |
1430 |
1431 |
1432 |
1433 |
1434 |
1435 |
1436 | 1437 | [ 1438 | 1439 | 1440 | "end" 1441 | 1442 | 1443 | ] = 1444 | 1445 |
1446 |
1447 | 59 1448 |
1449 |
1450 |
1451 |
1452 |
1453 |
1454 |
1455 |
1456 |
1457 | 1458 |
1459 | 1460 | [ 1461 | 1462 | 1463 | 11 1464 | 1465 | 1466 | ] = 1467 | 1468 |
1469 | 1470 | Table (3) 1471 | 1472 | 1473 | table: 0x55a88bb9a4f0 1474 | 1475 |
1476 |
1477 |
1478 |
1479 | 1480 | [ 1481 | 1482 | 1483 | "start" 1484 | 1485 | 1486 | ] = 1487 | 1488 |
1489 |
1490 | 61 1491 |
1492 |
1493 |
1494 |
1495 |
1496 |
1497 | 1498 |
1499 | 1500 | [ 1501 | 1502 | 1503 | "content" 1504 | 1505 | 1506 | ] = 1507 | 1508 |
1509 | 1510 | Table (7) 1511 | 1512 | 1513 | table: 0x55a88bb86a40 1514 | 1515 |
1516 |
1517 |
1518 |
1519 | 1520 | [ 1521 | 1522 | 1523 | 1 1524 | 1525 | 1526 | ] = 1527 | 1528 |
1529 |
1530 | "- @function extract :: input:string, language?:Language, options?:table" 1531 |
1532 |
1533 |
1534 |
1535 |
1536 |
1537 | 1538 | [ 1539 | 1540 | 1541 | 2 1542 | 1543 | 1544 | ] = 1545 | 1546 |
1547 |
1548 | "- Extracts comment from a string separated by newlines." 1549 |
1550 |
1551 |
1552 |
1553 |
1554 |
1555 | 1556 | [ 1557 | 1558 | 1559 | 3 1560 | 1561 | 1562 | ] = 1563 | 1564 |
1565 |
1566 | "# Available options" 1567 |
1568 |
1569 |
1570 |
1571 |
1572 |
1573 | 1574 | [ 1575 | 1576 | 1577 | 4 1578 | 1579 | 1580 | ] = 1581 | 1582 |
1583 |
1584 | " - `patterns:boolean` (`false`): Whether to use patterns for the language fields and ignore string or not." 1585 |
1586 |
1587 |
1588 |
1589 |
1590 |
1591 | 1592 | [ 1593 | 1594 | 1595 | 5 1596 | 1597 | 1598 | ] = 1599 | 1600 |
1601 |
1602 | ' - `ignore:string` (`"///"`): String used to determine when to start or stop ignoring comments.' 1603 |
1604 |
1605 |
1606 |
1607 |
1608 |
1609 | 1610 | [ 1611 | 1612 | 1613 | 6 1614 | 1615 | 1616 | ] = 1617 | 1618 |
1619 |
1620 | " - `merge:boolean` (`true`): Whether to merge adjacent single-line comments." 1621 |
1622 |
1623 |
1624 |
1625 |
1626 |
1627 | 1628 | [ 1629 | 1630 | 1631 | 7 1632 | 1633 | 1634 | ] = 1635 | 1636 |
1637 |
1638 | ' - `paragraphs:boolean` (`true`): Whether to split multi-line comments by empty strings (`""`).' 1639 |
1640 |
1641 |
1642 |
1643 |
1644 |
1645 |
1646 |
1647 |
1648 | 1649 | [ 1650 | 1651 | 1652 | "end" 1653 | 1654 | 1655 | ] = 1656 | 1657 |
1658 |
1659 | 67 1660 |
1661 |
1662 |
1663 |
1664 |
1665 |
1666 |
1667 |
1668 |
1669 |
1670 | 1671 | 1672 | --------------------------------------------------------------------------------