├── .eslintrc.json
├── .gitignore
├── CONFIGURING.md
├── LICENSE
├── README.md
├── USAGE.md
├── package.json
├── pnpm-lock.yaml
├── postbuild.js
├── src
├── cli
│ ├── index.ts
│ ├── resolver.ts
│ └── util.ts
├── config
│ ├── index.ts
│ ├── types.ts
│ └── validator.ts
├── filesystem.ts
├── log.ts
├── markdown
│ ├── inline.ts
│ ├── parser.ts
│ └── util.ts
├── types
│ ├── config.ts
│ └── markdown.ts
└── util.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | // todo: make better lol
3 | "env": {
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": ["eslint:recommended" ],
8 | "plugins": [ "@typescript-eslint" ],
9 | "parser": "@typescript-eslint/parser",
10 | "parserOptions": {
11 | "ecmaVersion": 12,
12 | "project": [ "./tsconfig.json" ],
13 | "sourceType": "module"
14 | },
15 | "rules": {
16 | "no-process-exit": "off",
17 | "no-undef": "off",
18 | "no-unused-vars": "off",
19 | "no-redeclare": "off",
20 | "@typescript-eslint/no-unused-vars": "error",
21 | "@typescript-eslint/no-redeclare": "error",
22 | "prefer-const": "error",
23 |
24 | // Possible errors
25 | "no-loss-of-precision": "error",
26 | "no-extra-parens": [
27 | "error", "all",
28 | {
29 | "conditionalAssign": false,
30 | "returnAssign": false,
31 | "nestedBinaryExpressions": false,
32 | "ignoreJSX": "multi-line"
33 | }
34 | ],
35 | "no-template-curly-in-string": "error",
36 | "no-sparse-arrays": "error",
37 | "no-unreachable-loop": "error",
38 | "no-useless-backreference": "error",
39 | "require-atomic-updates": "error",
40 |
41 | // Best practices
42 | "class-methods-use-this": "error",
43 | "default-case-last": "error",
44 | "default-param-last": "error",
45 | "dot-location": [ "error", "property" ],
46 | "dot-notation": "error",
47 | "eqeqeq": "error",
48 | "grouped-accessor-pairs": [ "error", "getBeforeSet" ],
49 | "guard-for-in": "error",
50 | "no-alert": "error",
51 | "no-caller": "error",
52 | "no-constructor-return": "error",
53 | "no-else-return": "error",
54 | "@typescript-eslint/no-empty-function": "error",
55 | "no-eval": "error",
56 | "no-extend-native": "error",
57 | "no-extra-bind": "error",
58 | "no-floating-decimal": "error",
59 | "no-implicit-coercion": "error",
60 | "no-implied-eval": "error",
61 | "no-invalid-this": "error",
62 | "no-iterator": "error",
63 | "no-labels": "error",
64 | "no-lone-blocks": "error",
65 | "no-loop-func": "error",
66 | "no-multi-spaces": "error",
67 | "no-new": "error",
68 | "no-new-wrappers": "error",
69 | "no-octal": "error",
70 | "no-octal-escape": "error",
71 | "no-proto": "error",
72 | "no-return-assign": [ "error", "except-parens" ],
73 | "no-return-await": "error",
74 | "no-self-compare": "error",
75 | "no-sequences": "error",
76 | "no-throw-literal": "error",
77 | "no-unmodified-loop-condition": "error",
78 | "no-unused-expressions": "error",
79 | "no-warning-comments": "warn",
80 | "prefer-promise-reject-errors": "error",
81 | "radix": "error",
82 | "wrap-iife": "error",
83 | "yoda": "error",
84 |
85 | // Vars
86 | "@typescript-eslint/no-shadow": "error",
87 | "no-undefined": "error",
88 | "@typescript-eslint/no-use-before-define": "error",
89 |
90 | // Stylistic
91 | "array-bracket-newline": [ "error", "consistent" ],
92 | "array-bracket-spacing": [ "error", "always" ],
93 | "block-spacing": "error",
94 | "brace-style": [ "error", "1tbs", { "allowSingleLine": true } ],
95 | // "camelcase": "error",
96 | "comma-dangle": [
97 | "error",
98 | {
99 | "arrays": "always-multiline",
100 | "objects": "always-multiline",
101 | "imports": "always-multiline",
102 | "exports": "always-multiline",
103 | "functions": "never"
104 | }
105 | ],
106 | "comma-spacing": "error",
107 | "comma-style": "error",
108 | "computed-property-spacing": "error",
109 | "eol-last": "error",
110 | "func-call-spacing": "error",
111 | "func-name-matching": "error",
112 | "function-call-argument-newline": [ "error", "consistent" ],
113 | "function-paren-newline": [ "error", "multiline-arguments" ],
114 | "indent": [ "error", 2, { "SwitchCase": 1 } ],
115 | "jsx-quotes": [ "error", "prefer-single" ],
116 | "key-spacing": "error",
117 | "keyword-spacing": "error",
118 | "linebreak-style": [ "error", "unix" ],
119 | "lines-between-class-members": "error",
120 | "multiline-ternary": [ "error", "always-multiline" ],
121 | "new-cap": "error",
122 | "new-parens": "error",
123 | "no-array-constructor": "error",
124 | "no-lonely-if": "error",
125 | "no-mixed-operators": "error",
126 | "no-multi-assign": "error",
127 | "no-multiple-empty-lines": "error",
128 | "no-new-object": "error",
129 | "no-tabs": "error",
130 | "no-trailing-spaces": "error",
131 | "no-unneeded-ternary": "error",
132 | "no-whitespace-before-property": "error",
133 | "nonblock-statement-body-position": "error",
134 | "object-curly-newline": [ "error", { "multiline": true } ],
135 | "object-curly-spacing": [ "error", "always" ],
136 | "object-property-newline": [ "error", { "allowAllPropertiesOnSameLine": true } ],
137 | "one-var": [ "error", "never" ],
138 | "operator-assignment": "error",
139 | "operator-linebreak": [ "error", "before" ],
140 | "padded-blocks": [ "error", "never" ],
141 | "prefer-exponentiation-operator": "error",
142 | "quotes": [ "error", "single" ],
143 | "semi": [ "error", "never" ],
144 | "semi-style": [ "error", "first" ],
145 | "space-before-blocks": "error",
146 | "space-before-function-paren": "error",
147 | "space-in-parens": "error",
148 | "space-infix-ops": "error",
149 | "space-unary-ops": [ "error", { "words": true, "nonwords": false } ],
150 | "switch-colon-spacing": "error",
151 | "template-tag-spacing": "error",
152 | "unicode-bom": "error",
153 |
154 | // ES6
155 | "arrow-body-style": "error",
156 | "arrow-parens": "error",
157 | "arrow-spacing": "error",
158 | "generator-star-spacing": [
159 | "error",
160 | {
161 | "named": "after",
162 | "anonymous": "after",
163 | "method": "before"
164 | }
165 | ],
166 | "no-useless-computed-key": "error",
167 | "no-useless-constructor": "error",
168 | "no-useless-rename": "error",
169 | "no-var": "error",
170 | "object-shorthand": [ "error", "never" ],
171 | "prefer-arrow-callback": "error",
172 | "prefer-rest-params": "error",
173 | "prefer-template": "error",
174 | "rest-spread-spacing": "error",
175 | "symbol-description": "error",
176 | "template-curly-spacing": "error",
177 | "yield-star-spacing": [ "error", "after" ]
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | .idea
3 | dist
4 | node_modules
5 | coverage
6 |
7 | *.nogit.*
8 | *.nogit/
9 |
10 | .testrun
11 |
--------------------------------------------------------------------------------
/CONFIGURING.md:
--------------------------------------------------------------------------------
1 | # Configuring Spoonfeed
2 | The config file is `spoonfeed.config.js` and must exports a valid config object, or else the compilation will fail.
3 | If missing, default config will apply.
4 |
5 | Config files should look like this (note that this is an example and none of the fields shown are required):
6 |
7 | ```js
8 | module.exports = {
9 | documents: {
10 | source: 'filesystem',
11 | path: 'docs'
12 | },
13 | build: {
14 | target: 'build'
15 | }
16 | }
17 | ```
18 |
19 | Note: Paths are relative to the config file.
20 |
21 | ## Configuring documents source
22 | Documents can be part of a category, although Spoonfeed doesn't support subcategories. Document without categories
23 | will always be shown at the top of the list.
24 |
25 | No matter the source you're pulling from, documents name and slug will be determined by the first Heading 1 encountered.
26 |
27 | - `documents.source` - string (default: `'filesystem'`)
28 | The document source to use. Spoonfeed supports two document sources: `filesystem` and `registry`.
29 |
30 | - `documents.assets` - string (default: `'assets'`)
31 | Folder where documents are located, relative to the config file.
32 |
33 | - `documents.path` - string (default: `'docs'`)
34 | Folder where documents are located, relative to the config file.
35 |
36 | ### Filesystem document source settings
37 | This is the default document source used by Spoonfeed. The specified folder will be scanned and the documentation tree
38 | will be built from this. You can force file sorting by prepending `[int]-` to the filename.
39 |
40 | By default, categories name and slug are determined based on the folder name. If you want to use a different name, you
41 | can do so by adding a `manifest.json` file in the folder. It can also contain additional data that'll be passed to
42 | the Preact component rendering time. Slug cannot be set.
43 |
44 | ###### Example manifest
45 | ```json
46 | { "name": "My Awesome Category" }
47 | ```
48 |
49 | ### Registry document source settings
50 | - `documents.documents` - array
51 | List of documents and categories
52 |
53 |
54 |
55 | ## Basic UI settings
56 | - `ui.title` - string (default: `'Documentation'`)
57 | Title of your documentation. Will be shown in tab title and used in SEO tags.
58 |
59 | - `ui.description` - string (default: `'A documentation generated by Spoonfeed'`)
60 | Description of your documentation. Will be used in SEO tags.
61 |
62 | - `ui.copyright` - string? (default: `null`)
63 | Defines the copyright notice shown on the web page. `null` means no copyright notice. To get the current year, you can
64 | put `{{ year }}`.
65 | \- Example: `© {{ year }} Borkenware`
66 |
67 | - `ui.logo` - string? (default: `null`)
68 | Logo shown at the top of the documentation. If `null`, the title will be shown in plaintext.
69 |
70 | - `ui.favicon` - string? (default: `null`)
71 | Icon shown in browser's tabs. We recommend a 128x128 PNG file, since this will be used in SEO tags. An ICO file will be
72 | generated for the actual favicon.
73 |
74 | - `ui.acknowledgements` - boolean (default: `true`)
75 | Whether the "Acknowledgements" page listing dependencies and their license should be generated. This feature ensure
76 | compliance regarding third party software licenses and the preservation of copyright notices.
77 |
78 | ## Build options
79 | - `build.target` - string (default: `'build'`)
80 | Where built assets will be saved.
81 |
82 | - `build.mode` - string (default: `'preact'`)
83 | What the final result should compile to. Possible values are:
84 | - `preact` (Default)
85 | - `html` (**Not implemented**)
86 |
87 | - `build.sourcemaps` - boolean (default: `true`)
88 | Whether source maps should be generated or not.
89 |
90 | - `build.optimizeImg` - boolean (default: `true`)
91 | Whether images should be optimized or not.
92 |
93 | - `build.offline` - boolean (default: `true`)
94 | Whether Spoonfeed should generate the necessary service worker and manifest to make the webpage available to users
95 | even if they go offline (provided they visited the docs at least once prior) or not.
96 |
97 | - `build.mangle` - boolean (default: `true`)
98 | Whether exports should be mangled or not. Only applicable for `preact` builds.
99 |
100 | - `build.split` - boolean (default: `true`)
101 | Whether Spoonfeed should make use of code splitting or not. Reduces the initial bundle size. Only applicable for
102 | `preact` builds.
103 |
104 | - `build.devtools` - boolean (default: `true`)
105 | Whether Spoonfeed should include Preact devtools or not. Disabling it will result in a smaller bundle.
106 |
107 | ## Pre-rendering server-side options
108 | **Note**: Those will only be used if you set your app to compile to a Preact app.
109 |
110 | - `ssr.generate` - boolean (default: `false`)
111 | Whether Spoonfeed should generate a web server for pre-rendering the Preact app.
112 |
113 | - `ssr.http2` - boolean (default: `false`)
114 | Whether the HTTP server should be a HTTP/2 server. Requires a valid SSL certificate.
115 | **Note**: When enabled, browsers not supporting HTTP/2 will be unable to connect.
116 |
117 | - `ssr.redirectInsecure` - boolean (default: `false`)
118 | Redirects plain HTTP requests to HTTPS. Requires a valid SSL certificate.
119 |
120 | - `ssr.ssl.key` - string
121 | Path to the SSL private key. It'll be copied in the build target folder.
122 |
123 | - `ssr.ssl.cert` - string
124 | Path to the SSL certificate. It'll be copied in the build target folder.
125 |
126 | ### Why generate a server?
127 | Although Preact apps are fantastic, they absolutely require the target device to run JS code to function. While this
128 | isn't much of a problem nowadays, it still means it'll take longer for the first contents to be displayed to the end
129 | user. This gives a feeling of the app taking forever to load, and it affects your SEO score (and even worse, not all
130 | search engines run JS code when crawling).
131 |
132 | To remedy those issues while keeping the advantages of client-side-rendering, you can give the user a pre-rendered
133 | version of your website, which means content will be instantly available, and then hydrate the page to bind events
134 | and start using client-side-rendering from this point on. It also means your webpage doesn't require JavaScript to
135 | be usable.
136 |
137 | ## Basic theme configuration
138 | - `theme.theme` - TBD
139 | - `theme.colors` - TBD
140 |
141 | ## Advanced theme & UI configuration
142 | TBD
143 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020-2021 Borkenware, All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright notice, this
7 | list of conditions and the following disclaimer.
8 | 2. Redistributions in binary form must reproduce the above copyright notice,
9 | this list of conditions and the following disclaimer in the
10 | documentation and/or other materials provided with the distribution.
11 | 3. Neither the name of the copyright holder nor the names of its contributors
12 | may be used to endorse or promote products derived from this software without
13 | specific prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spoonfeed
2 | [](https://github.com/borkenware/spoonfeed/blob/mistress/LICENSE)
3 |
4 | Generate beautiful documentation portals in seconds from easy-to-edit markdown files. Bundle your markdown files
5 | into a Preact app easily distributable on the World Wide Web:tm:
6 |
7 | # NOTE
8 | This project is a heavy WIP and is not usable yet.
9 |
10 | --------
11 |
12 | ## Installation
13 | This package hasn't been published yet.
14 |
26 |
27 | See USAGE.md for information about how to use this.
28 |
29 | ## Why?
30 | Writing documentation is already a boring process, and the documentation team may not have time to put into
31 | distributing it in an easy-to-access format.
32 |
33 | Spoonfeed takes care of that for you or your team. Focus on writing crystal clear documentation, in an easy-to-edit
34 | format, and let Spoonfeed take care of the rest.
35 |
36 | Originally, a few Borkenware projects used their own rice of documentation generator which was a pain since they
37 | all shared slight differences. We decided to unify all of our pieces in a unified tool that not only us can benefit
38 | from.
39 |
40 | ## The advantages
41 | Spoonfeed's design has been really influenced by how [Discord](https://discord.com) structured their API documentation.
42 | However, we were not super convinced by the approach they have been taking to incorporate those markdown files in
43 | their Developer Portal.
44 |
45 | In their Developer Portal, Discord sends the raw markdown to the clients, and then their React application uses
46 | Simple Markdown and HLJS to render the portal contents. While this is easy to implement, this means the client
47 | has to download and run way more JS code than it should: according to Bundlephobia, HLJS is 93.6kB minified, and
48 | Simple Markdown 14.9kB. Plus the weight of the markdown blob.
49 |
50 | What Spoonfeed does is it parses the markdown files at compile time, and outputs a plain Preact component. This means
51 | we no longer need libraries to parse our markdown at-runtime, reducing the load times and the processing power required
52 | (which is a valuable resource for mobile devices).
53 |
54 | ## Why using Preact?
55 | We love React at Borkenware, but it unfortunately often ends up in rather large bundles, which is not justified here
56 | since we don't use a lot of React features. So we've decided to ship Preact, to have the best of the two worlds.
57 |
58 | We will in the future also support generating plain html files, for compatibility with GitHub pages or if you prefer
59 | it that way. They will most likely come with a reduced feature set, though.
60 |
61 | ## Why "Spoonfeed"?
62 | GitHub suggested `fictional-spoon` when creating the repo. No it's not a joke it's actually how it got this name.
63 |
--------------------------------------------------------------------------------
/USAGE.md:
--------------------------------------------------------------------------------
1 | # Using Spoonfeed
2 | ## Writing markdown
3 | Spoonfeed uses a markdown superset to give more possibility to documentation writers. This superset has been designed
4 | to be easy to read for people reading the plain markdown files, while giving more possibilities.
5 |
6 | We use a completely custom parser, so it may or may not comply with the Markdown standard at 100%. We strongly
7 | recommend following all the best practices to avoid parsing issues (and it'll even be beneficial for you!).
8 |
9 | ### Underline
10 | Our markdown superset supports underlining using a "semi-standard" syntax used in a lot of existing superset:
11 | `__Underlined content__`.
12 |
13 | ### Document Linking
14 | To make references between documents, you can simply use a link tag, where the link is in the following format:
15 | ```
16 | Uncategorized document:
17 | ##document-file-name
18 |
19 | Categorized document:
20 | ##category-slug/document-file-name
21 |
22 | Final link:
23 | [Look this document](##important/information)
24 |
25 | With an anchor:
26 | [Look this document](##important/information#legal)
27 | ```
28 |
29 | The document file name must not include the sorting prefix (`-`), and the link will not be parsed if the document
30 | doesn't exist. Spoonfeed will issue a warning in that case.
31 |
32 | ### Alert Boxes
33 | Alert boxes are blocks of text that'll be emphasized, to give the user either an important information or to warn
34 | them about something.
35 |
36 | They are achieved by using a block quote which has on its first line either `info`, `warn` or `danger`.
37 | ```
38 | >info
39 | > This is an informative alert box
40 | ```
41 |
42 | ### Videos
43 | Spoonfeed lets you import videos, either using plain media files, or YouTube videos. This can be useful to showcase
44 | something depending on what you're documenting.
45 |
46 | ```
47 | !!v[https://www.youtube.com/watch?v=Tt7bzxurJ1I]
48 | !!v[/videos/test.mp4]
49 | ```
50 |
51 | ### H6 Headings
52 | Spoonfeed uses H6 headings as table and code blocks headers rather than the unused h6 block. We strongly recommend
53 | labelling all your tables & code blocks with them.
54 |
55 | ### HTTP Routes
56 | Spoonfeed has support to highlight HTTP routes, which will be really useful if you are documenting a REST API.
57 | HTTP routes should be prefixed with `%%`, then followed by their method in all uppercase, and then the path (which
58 | should by convention exclude base path for readability). Route parameters must be wrapped in curly brackets.
59 |
60 | ```
61 | %% POST /test/path/{parameter}/yes
62 | ```
63 |
64 | ### Codeblocks
65 | Spoonfeed runs syntax highlighting using [shiki](https://shiki.matsu.io/) to give some color:tm: to your code.
66 | It also shows line numbers for easier readability and referencing (Who wants to count lines to understand what that
67 | person meant by "look line 69"?).
68 |
69 | ### Local resources
70 | When embedding let's say images, it's rare to have them already hosted and you most likely have them next to
71 | your documents. When embedding a resource, if Spoonfeed detects it's a resource on the filesystem, it'll import
72 | it, optimize the resource and save it to the build folder.
73 |
74 | Spoonfeed considers (by default, this is configurable) that all the assets will be in `/assets`. For example,
75 | let's take the following image declaration:
76 | ```md
77 | 
78 | ```
79 | Spoonfeed will try to import the file `/assets/photos/cat.jpg`, and do all the required magic in background.
80 | If the file is not found, an error will be thrown.
81 |
82 | ## Building the Web interface
83 | Spoonfeed outputs a bundled [Preact](https://preactjs.com) application, and uses [Rollup](http://rollupjs.org)
84 | internally for bundling.
85 |
86 | ### Configuration
87 | Since there is a lot you can configure, this is documented in a separate document. See CONFIGURING.md for more details
88 | on how to configure Spoonfeed.
89 |
90 | ### Development
91 | During development you'll be able to see live changes you do to the documentation to have visual feedback about
92 | what you're doing. You can also see live changes to the configuration to some extent. Some configuration changes
93 | do require a complete restart.
94 | ```
95 | spoonfeed serve
96 | ```
97 |
98 | ### Bundling
99 | To bundle the Preact app and get ready to deploy it, make sure your configuration is ready and run
100 | ```
101 | spoonfeed bundle
102 | ```
103 | The outputs will be in the `build` directory (unless overridden in the config). Once that's done, you have 2 methods
104 | of putting your docs online, depending on how you configured Spoonfeed:
105 |
106 | #### Without server-side pre-rendering
107 | You simply need to serve all the contents of the build folder. Just set your web server document root to that
108 | and fire it up. We recommend setting a really high cache lifetime for everything served from `/dist/`.
109 |
110 | #### With server-side pre-rendering
111 | If you enabled server-side pre-rendering, then Spoonfeed did output a functional Node application in the
112 | `build` folder. The node application is ready for production usage and contains the necessary security features
113 | for such use.
114 |
115 | First, you need to install dependencies by running `pnpm i` (or the install command of your preferred package
116 | manager) in the build folder.
117 |
118 | Then, you can fire up the server simply by running `node server.js`. By default, it'll listen to `0.0.0.0:80` (or
119 | `0.0.0.0:443` if SSL is enabled), but this can be changed using the following environment vars:
120 | - `SPOONFEED_BIND_ADDR`: Controls the interface Spoonfeed listens to.
121 | - `SPOONFEED_BIND_PORT`: Controls the port Spoonfeed listens to.
122 |
123 | If HTTPS upgrades are enabled, a plain HTTP server will be listen to `:80` and redirect incoming
124 | requests to the HTTPS server. The port cannot be changed.
125 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@borkenware/spoonfeed",
3 | "version": "0.1.0",
4 | "description": "Generate beautiful documentation portals in seconds from easy-to-edit markdown files.",
5 | "type": "module",
6 | "main": "dist/spoonfeed.js",
7 | "repository": "git@github.com:borkenware/spoonfeed.git",
8 | "author": "Cynthia ",
9 | "license": "BSD-3-Clause",
10 | "engines": {
11 | "node": ">= 14"
12 | },
13 | "scripts": {
14 | "lint": "eslint src --ext ts",
15 | "build": "tsc && node postbuild.js"
16 | },
17 | "dependencies": {
18 | "@babel/core": "^7.12.10",
19 | "@babel/plugin-transform-runtime": "^7.12.10",
20 | "@babel/preset-env": "^7.12.11",
21 | "@babel/runtime": "^7.12.5",
22 | "@rollup/plugin-babel": "^5.2.2",
23 | "@rollup/plugin-commonjs": "^17.0.0",
24 | "@rollup/plugin-node-resolve": "^11.1.0",
25 | "preact": "^10.5.11",
26 | "preact-helmet": "^4.0.0-alpha-3",
27 | "preact-router": "^3.2.1",
28 | "rollup": "^2.38.0",
29 | "rollup-plugin-terser": "^7.0.2",
30 | "sass": "^1.32.5",
31 | "shiki": "^0.2.7"
32 | },
33 | "devDependencies": {
34 | "@types/node": "^14.14.22",
35 | "@types/sass": "^1.16.0",
36 | "@typescript-eslint/eslint-plugin": "^4.14.1",
37 | "@typescript-eslint/parser": "^4.14.1",
38 | "eslint": "^7.18.0",
39 | "ts-node": "^9.1.1",
40 | "typescript": "^4.1.3"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/postbuild.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020-2021 Borkenware, All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | *
7 | * 1. Redistributions of source code must retain the above copyright notice, this
8 | * list of conditions and the following disclaimer.
9 | * 2. Redistributions in binary form must reproduce the above copyright notice,
10 | * this list of conditions and the following disclaimer in the
11 | * documentation and/or other materials provided with the distribution.
12 | * 3. Neither the name of the copyright holder nor the names of its contributors
13 | * may be used to endorse or promote products derived from this software without
14 | * specific prior written permission.
15 | *
16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | import { readFileSync, writeFileSync } from 'fs'
29 | import { execSync } from 'child_process'
30 |
31 | // Replace contributors count
32 | const contribCount = execSync('git --no-pager shortlog -s -n --no-merges', { stdio: [ 'inherit', 'pipe', 'pipe' ] })
33 | .toString()
34 | .split('\n')
35 | .filter(Boolean)
36 | .length
37 |
38 | const sfCli = new URL('./dist/cli/index.js', import.meta.url)
39 | writeFileSync(sfCli, readFileSync(sfCli, 'utf8').replace('##{CONTRIBUTORS}', contribCount), 'utf8')
40 |
--------------------------------------------------------------------------------
/src/cli/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /*
4 | * Copyright (c) 2020-2021 Borkenware, All rights reserved.
5 | *
6 | * Redistribution and use in source and binary forms, with or without
7 | * modification, are permitted provided that the following conditions are met:
8 | *
9 | * 1. Redistributions of source code must retain the above copyright notice, this
10 | * list of conditions and the following disclaimer.
11 | * 2. Redistributions in binary form must reproduce the above copyright notice,
12 | * this list of conditions and the following disclaimer in the
13 | * documentation and/or other materials provided with the distribution.
14 | * 3. Neither the name of the copyright holder nor the names of its contributors
15 | * may be used to endorse or promote products derived from this software without
16 | * specific prior written permission.
17 | *
18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 | */
29 |
30 | import log from '../log.js'
31 |
32 | // eslint-disable-next-line @typescript-eslint/no-var-requires -- We use require here so it doesn't get bundled by TS
33 | const { version: VERSION } = require('../../package.json') as { version: string }
34 |
35 | function bundle (serve: boolean): void {
36 | if (serve) {
37 | console.log('Not implemented yet!')
38 | process.exit(0)
39 | }
40 |
41 | console.log('Henlo')
42 | }
43 |
44 | function about (): void {
45 | console.log('Proudly built by Borkenware.')
46 | // some day -- console.log('Proudly built by Borkenware, and ##{CONTRIBUTORS} contributors.')
47 | console.log(`Spoonfeed is Honest Open ${Math.random().toFixed(3) === '0.420' ? 'Sauce' : 'Source'} Software, licensed under BSD-3-Clause.`)
48 | console.log('https://github.com/borkenware/spoonfeed')
49 | }
50 |
51 | function displayHelp (): void {
52 | console.log('spoonfeed [arguments] \n')
53 |
54 | console.log(' - spoonfeed bundle')
55 | console.log(' Bundles the documentation')
56 | console.log(' Enable debug logging with --debug')
57 | console.log()
58 | /*
59 | * console.log(' - spoonfeed serve')
60 | * console.log(' Starts the dev server')
61 | * console.log(' Enable debug logging with --debug')
62 | * console.log()
63 | */
64 | console.log(' - spoonfeed about')
65 | console.log(' Prints information about Spoonfeed')
66 | console.log()
67 | console.log(' - spoonfeed help')
68 | console.log(' Shows this help message')
69 | }
70 |
71 | if (require.main?.filename === __filename) {
72 | console.log(`Spoonfeed v${VERSION}`, '\n')
73 |
74 | if (process.argv.includes('--debug')) log.setDebug(true)
75 | const command = process.argv.length > 2 ? process.argv[process.argv.length - 1] : ''
76 | switch (command) {
77 | case 'bundle':
78 | bundle(false)
79 | break
80 | case 'serve':
81 | bundle(true)
82 | break
83 | case 'about':
84 | about()
85 | break
86 | default:
87 | if (command && command !== 'help') {
88 | console.log(`Invalid usage! Unknown command "${command}".`)
89 | console.log('Valid usages are:\n')
90 | }
91 |
92 | displayHelp()
93 | break
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/cli/resolver.ts:
--------------------------------------------------------------------------------
1 | // todo: resolve useful stuff for cli (config, pkg manager, ...)
2 |
--------------------------------------------------------------------------------
/src/cli/util.ts:
--------------------------------------------------------------------------------
1 | // todo: config reader, dep install
2 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020-2021 Borkenware, All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | *
7 | * 1. Redistributions of source code must retain the above copyright notice, this
8 | * list of conditions and the following disclaimer.
9 | * 2. Redistributions in binary form must reproduce the above copyright notice,
10 | * this list of conditions and the following disclaimer in the
11 | * documentation and/or other materials provided with the distribution.
12 | * 3. Neither the name of the copyright holder nor the names of its contributors
13 | * may be used to endorse or promote products derived from this software without
14 | * specific prior written permission.
15 | *
16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | import type { Config } from '../types/config.js'
29 |
30 | import { existsSync } from 'fs'
31 | import { dirname, join } from 'path'
32 |
33 | import { extendedTypeof } from '../util.js'
34 | import validate from './validator.js'
35 |
36 | interface ConfigPath { cfg: string | null, dir: string }
37 |
38 | const BaseConfig: Config = {
39 | workdir: '',
40 | documents: {
41 | source: 'filesystem',
42 | path: 'docs',
43 | assets: 'assets',
44 | },
45 | ui: {
46 | title: 'Documentation',
47 | description: 'A documentation generated by Spoonfeed',
48 | copyright: null,
49 | logo: null,
50 | favicon: null,
51 | acknowledgements: true,
52 | },
53 | build: {
54 | target: 'build',
55 | mode: 'preact',
56 | optimizeImg: true,
57 | offline: true,
58 | mangle: true,
59 | split: true,
60 | },
61 | ssr: {
62 | generate: false,
63 | redirectInsecure: false,
64 | http2: false,
65 | ssl: null,
66 | },
67 | }
68 |
69 | function mergeDeep (target: Record, ...sources: Array>): Record {
70 | if (!sources.length) return target
71 | const source = sources.shift()
72 |
73 | for (const key in source) {
74 | if (extendedTypeof(source[key]) === 'object') {
75 | if (!target[key]) Object.assign(target, { [key]: {} })
76 | mergeDeep(target[key] as Record, source[key] as Record)
77 | } else {
78 | Object.assign(target, { [key]: source[key] })
79 | }
80 | }
81 |
82 | return mergeDeep(target, ...sources)
83 | }
84 |
85 | export function findConfig (dir: string | null = null): ConfigPath {
86 | if (!dir) dir = process.cwd()
87 |
88 | if (existsSync(join(dir, 'spoonfeed.config.js'))) {
89 | return { cfg: join(dir, 'spoonfeed.config.js'), dir: dir }
90 | }
91 |
92 | if (existsSync(join(dir, 'package.json'))) {
93 | return { cfg: null, dir: dir }
94 | }
95 |
96 | const next = dirname(dir)
97 | if (next === dir) {
98 | // We reached system root
99 | return { cfg: null, dir: dir }
100 | }
101 |
102 | return findConfig(next)
103 | }
104 |
105 | export default async function readConfig (): Promise {
106 | let cfg: Record | void = {}
107 | const path = findConfig()
108 | if (path.cfg) {
109 | try {
110 | cfg = await import(path.cfg).then((m) => m?.default) as Record | void
111 | } catch (e) {
112 | throw new SyntaxError(e)
113 | }
114 |
115 | if (!cfg) throw new Error('No config was exported')
116 | validate(cfg)
117 | }
118 |
119 | const config = {}
120 | mergeDeep(config, BaseConfig, cfg, { workdir: path.dir })
121 | return config as Config
122 | }
123 |
--------------------------------------------------------------------------------
/src/config/types.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020-2021 Borkenware, All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | *
7 | * 1. Redistributions of source code must retain the above copyright notice, this
8 | * list of conditions and the following disclaimer.
9 | * 2. Redistributions in binary form must reproduce the above copyright notice,
10 | * this list of conditions and the following disclaimer in the
11 | * documentation and/or other materials provided with the distribution.
12 | * 3. Neither the name of the copyright holder nor the names of its contributors
13 | * may be used to endorse or promote products derived from this software without
14 | * specific prior written permission.
15 | *
16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | export interface ConfigDocumentsFs {
29 | source: 'filesystem'
30 | assets: string
31 | path: string
32 | }
33 |
34 | export interface ConfigDocumentsRegistry {
35 | source: 'registry'
36 | assets: string
37 | path: string
38 | documents: RawDocumentRegistry
39 | }
40 |
41 | export interface RegistryCategory {
42 | category: string
43 | documents: string[]
44 | }
45 |
46 | export type RawDocumentRegistry = Array
47 |
48 | export type DocumentRegistry = {
49 | documentCount: number
50 | documents: RawDocumentRegistry
51 | }
52 |
53 | export interface ConfigUi {
54 | title: string
55 | description: string
56 | copyright: string | null
57 | logo: string | null
58 | favicon: string | null
59 | acknowledgements: boolean
60 | }
61 |
62 | export type BuildMode = 'preact'
63 |
64 | export interface ConfigBuild {
65 | target: string
66 | mode: BuildMode
67 | optimizeImg: boolean
68 | offline: boolean
69 | mangle: boolean
70 | split: boolean
71 | }
72 |
73 | export interface ConfigSsr {
74 | generate: boolean
75 | redirectInsecure: boolean
76 | http2: false
77 | ssl: null | {
78 | cert: string
79 | key: string
80 | }
81 | }
82 |
83 | export interface ConfigSsrH2 {
84 | generate?: boolean
85 | redirectInsecure?: boolean
86 | http2: true
87 | ssl: {
88 | cert: string
89 | key: string
90 | }
91 | }
92 |
93 | export interface Config extends Record {
94 | workdir: string
95 | documents: ConfigDocumentsFs | ConfigDocumentsRegistry
96 | ui: ConfigUi
97 | build: ConfigBuild
98 | ssr: ConfigSsr | ConfigSsrH2
99 | }
100 |
--------------------------------------------------------------------------------
/src/config/validator.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020-2021 Borkenware, All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | *
7 | * 1. Redistributions of source code must retain the above copyright notice, this
8 | * list of conditions and the following disclaimer.
9 | * 2. Redistributions in binary form must reproduce the above copyright notice,
10 | * this list of conditions and the following disclaimer in the
11 | * documentation and/or other materials provided with the distribution.
12 | * 3. Neither the name of the copyright holder nor the names of its contributors
13 | * may be used to endorse or promote products derived from this software without
14 | * specific prior written permission.
15 | *
16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | import type { ExtendedType } from '../util.js'
29 | import { extendedTypeof } from '../util.js'
30 | import type { Config } from './types.js'
31 |
32 | type Schema = Record
33 |
34 | type SchemaItem = SchemaItemType | SchemaItemTypeArray | SchemaItemValues | SchemaItemNested
35 |
36 | interface SchemaItemType {
37 | required?: boolean | ((cfg: Config | null) => boolean)
38 | types: ExtendedType | ExtendedType[]
39 | }
40 |
41 | interface SchemaItemTypeArray {
42 | required?: boolean | ((cfg: Config | null) => boolean)
43 | types: 'array'
44 | of: SchemaItem[]
45 | }
46 |
47 | interface SchemaItemValues {
48 | required?: boolean | ((cfg: Config | null) => boolean)
49 | values: string[]
50 | }
51 |
52 | interface SchemaItemNested {
53 | required?: boolean | ((cfg: Config | null) => boolean)
54 | schema: Schema
55 | }
56 |
57 | enum ErrorTypes { TYPE, VALUE, ARRAY }
58 |
59 | const schema: Schema = {
60 | documents: {
61 | schema: {
62 | source: { values: [ 'filesystem', 'registry' ] },
63 | assets: { types: 'string' },
64 | // Filesystem specific
65 | path: { types: 'string' },
66 | // Registry specific
67 | documents: {
68 | types: 'array',
69 | of: [
70 | { types: 'string' },
71 | {
72 | schema: {
73 | category: { types: 'string' },
74 | documents: { types: 'array', of: [ { types: 'string' } ] },
75 | },
76 | },
77 | ],
78 | },
79 | },
80 | },
81 | ui: {
82 | schema: {
83 | title: { types: 'string' },
84 | description: { types: 'string' },
85 | copyright: { types: [ 'string', 'null' ] },
86 | logo: { types: [ 'string', 'null' ] },
87 | favicon: { types: [ 'string', 'null' ] },
88 | acknowledgements: { types: 'boolean' },
89 | },
90 | },
91 | build: {
92 | schema: {
93 | target: { types: 'string' },
94 | mode: { values: [ 'preact' ] },
95 | optimizeImg: { types: 'boolean' },
96 | offline: { types: 'boolean' },
97 | mangle: { types: 'boolean' },
98 | split: { types: 'boolean' },
99 | },
100 | },
101 | ssr: {
102 | schema: {
103 | generate: { types: 'boolean' },
104 | redirectInsecure: { types: 'boolean' },
105 | http2: { types: 'boolean' },
106 | ssl: {
107 | required: (c): boolean => Boolean(c?.ssr?.http2 || c?.ssr?.redirectInsecure),
108 | schema: {
109 | cert: { required: true, types: 'string' },
110 | key: { required: true, types: 'string' },
111 | },
112 | },
113 | },
114 | },
115 | }
116 |
117 | function validateType (value: unknown, item: SchemaItem, full: Record, prefix: string, tc: boolean = false): ErrorTypes | void {
118 | /* istanbul ignore else */
119 | if ('types' in item) {
120 | const types = Array.isArray(item.types) ? item.types : [ item.types ]
121 | const type = extendedTypeof(value)
122 | if (!types.includes(type)) return ErrorTypes.TYPE
123 |
124 | if (type === 'array' && 'of' in item) {
125 | const array = value as unknown[]
126 | for (const val of array) {
127 | if (item.of.every((sc) => typeof validateType(val, sc, full, prefix, true) !== 'undefined')) {
128 | return ErrorTypes.ARRAY
129 | }
130 | }
131 | }
132 | } else if ('values' in item) {
133 | if (!item.values.includes(value as string)) return ErrorTypes.VALUE
134 | } else if ('schema' in item) {
135 | try {
136 | // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Recursive call
137 | validateSchema(item.schema, value as Record, full, prefix)
138 | } catch (e) {
139 | if (tc) return ErrorTypes.ARRAY
140 | throw e
141 | }
142 | }
143 | }
144 |
145 | function validateSchema (schem: Schema, object: Record, full?: Record, prefix: string = ''): void {
146 | const rootType = extendedTypeof(object)
147 | if (rootType !== 'object') {
148 | throw new TypeError(`Invalid field type: expected object, got ${rootType} for ${prefix || 'root'}`)
149 | }
150 |
151 | if (!full) full = object
152 | for (const [ key, item ] of Object.entries(schem)) {
153 | const required = item.required
154 | ? typeof item.required === 'function' ? item.required(full as unknown as Config) : item.required
155 | : false
156 |
157 | if (key in object) {
158 | const error = validateType(object[key], item, full, `${prefix}${key}.`)
159 | switch (error) {
160 | case ErrorTypes.TYPE: {
161 | const i = item as SchemaItemType
162 | const types = Array.isArray(i.types) ? i.types : [ i.types ]
163 | throw new TypeError(`Invalid field type: expected ${types.join(' or ')}, got ${extendedTypeof(object[key])} for ${prefix}${key}`)
164 | }
165 | case ErrorTypes.VALUE: {
166 | const i = item as SchemaItemValues
167 | throw new TypeError(`Invalid field value: expected ${i.values.map((s: string) => `"${s}"`).join(' or ')}, got ${object[key] as string} for ${prefix}${key}`)
168 | }
169 | case ErrorTypes.ARRAY:
170 | throw new TypeError(`Invalid array item value for ${prefix}${key}`)
171 | }
172 | } else if (required) {
173 | throw new TypeError(`Missing required field ${prefix}${key}`)
174 | }
175 | }
176 | }
177 |
178 | export default function validate (config: Record): void {
179 | validateSchema(schema, config)
180 | }
181 |
--------------------------------------------------------------------------------
/src/filesystem.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020-2021 Borkenware, All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | *
7 | * 1. Redistributions of source code must retain the above copyright notice, this
8 | * list of conditions and the following disclaimer.
9 | * 2. Redistributions in binary form must reproduce the above copyright notice,
10 | * this list of conditions and the following disclaimer in the
11 | * documentation and/or other materials provided with the distribution.
12 | * 3. Neither the name of the copyright holder nor the names of its contributors
13 | * may be used to endorse or promote products derived from this software without
14 | * specific prior written permission.
15 | *
16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | import { join } from 'path'
29 | import { existsSync } from 'fs'
30 | import { stat, readdir as fsReaddir } from 'fs/promises'
31 |
32 | import type { DocumentRegistry, RawDocumentRegistry } from './config/types.js'
33 |
34 | const md = /\.(md|markdown)$/i
35 |
36 | interface ReaddirResult {
37 | folders: string[]
38 | files: string[]
39 | }
40 |
41 | async function readdir (folder: string): Promise {
42 | const files: string[] = []
43 | const folders: string[] = []
44 |
45 | for (const file of await fsReaddir(folder)) {
46 | const fileStat = await stat(join(folder, file))
47 | if (fileStat.isDirectory()) {
48 | folders.push(file)
49 | } else if (md.test(file)) {
50 | files.push(file)
51 | }
52 | }
53 |
54 | return { files: files, folders: folders }
55 | }
56 |
57 | export function validateRegistry (basepath: string, registry: RawDocumentRegistry): boolean {
58 | for (const r of registry) {
59 | if (typeof r === 'string') {
60 | if (!existsSync(join(basepath, r))) return false
61 | } else {
62 | const base = join(basepath, r.category)
63 | for (const d of r.documents) {
64 | if (!existsSync(join(base, d))) return false
65 | }
66 | }
67 | }
68 |
69 | return true
70 | }
71 |
72 | export default async function fsToRegistry (basepath: string): Promise {
73 | const registry: DocumentRegistry = {
74 | documentCount: 0,
75 | documents: [],
76 | }
77 |
78 | const res = await readdir(basepath)
79 | registry.documentCount += res.files.length
80 | registry.documents.push(...res.files.map((f) => join(basepath, f)))
81 | for (const folder of res.folders) {
82 | const { files } = await readdir(join(basepath, folder))
83 | const documents = files.map((f) => join(basepath, folder, f))
84 | registry.documentCount += documents.length
85 | registry.documents.push({ category: folder, documents: documents })
86 | }
87 |
88 | return registry
89 | }
90 |
--------------------------------------------------------------------------------
/src/log.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020-2021 Borkenware, All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | *
7 | * 1. Redistributions of source code must retain the above copyright notice, this
8 | * list of conditions and the following disclaimer.
9 | * 2. Redistributions in binary form must reproduce the above copyright notice,
10 | * this list of conditions and the following disclaimer in the
11 | * documentation and/or other materials provided with the distribution.
12 | * 3. Neither the name of the copyright holder nor the names of its contributors
13 | * may be used to endorse or promote products derived from this software without
14 | * specific prior written permission.
15 | *
16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | let logDebug = false
29 |
30 | const Prefixes = {
31 | DEBUG: '\x1b[47m\x1b[30m DEBUG \x1b[0m',
32 | INFO: '\x1b[44m\x1b[30m INFO \x1b[0m',
33 | SUCCESS: '\x1b[42m\x1b[30m SUCCESS \x1b[0m',
34 | WARNING: '\x1b[43m\x1b[30m WARN \x1b[0m',
35 | ERROR: '\x1b[41m\x1b[30m ERROR \x1b[0m',
36 | }
37 |
38 | function setDebug (d: boolean): void { logDebug = d }
39 |
40 | function debug (message: string): void {
41 | if (logDebug) {
42 | console.log(`${Prefixes.DEBUG} ${message}`)
43 | }
44 | }
45 |
46 | function info (message: string): void {
47 | console.log(`${Prefixes.INFO} ${message}`)
48 | }
49 |
50 | function success (message: string): void {
51 | console.log(`${Prefixes.SUCCESS} ${message}`)
52 | }
53 |
54 | function warn (message: string): void {
55 | console.log(`${Prefixes.WARNING} ${message}`)
56 | }
57 |
58 | function error (message: string, err?: Error): void {
59 | console.log(`${Prefixes.ERROR} ${message}`)
60 | if (err) {
61 | console.log(error) // todo: better
62 | }
63 | }
64 |
65 | export default {
66 | setDebug: setDebug,
67 | debug: debug,
68 | info: info,
69 | success: success,
70 | warn: warn,
71 | error: error,
72 | }
73 |
--------------------------------------------------------------------------------
/src/markdown/inline.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020-2021 Borkenware, All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | *
7 | * 1. Redistributions of source code must retain the above copyright notice, this
8 | * list of conditions and the following disclaimer.
9 | * 2. Redistributions in binary form must reproduce the above copyright notice,
10 | * this list of conditions and the following disclaimer in the
11 | * documentation and/or other materials provided with the distribution.
12 | * 3. Neither the name of the copyright holder nor the names of its contributors
13 | * may be used to endorse or promote products derived from this software without
14 | * specific prior written permission.
15 | *
16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | import { parseInline } from './util.js'
29 | import { MarkdownType } from '../types/markdown.js'
30 | import type { MarkdownNode, ParsedNode, LinkMarkdownNode, DocumentMarkdownNode, ImageMarkdownNode, VideoMarkdownNode } from '../types/markdown.js'
31 |
32 | /* eslint-disable @typescript-eslint/naming-convention -- ESLint config needs fix, see borkenware/eslint-config#1 */
33 | const LINK_PATH = '((\\/[\\+~%\\/\\.\\w\\-_]*)?\\??([\\-\\+=&;%@\\.\\w_]*)#?([\\.\\!\\/\\\\\\w-]*))'
34 | const LINK = `((([a-z]{3,9}:(\\/\\/)?)([\\-;:&=\\+\\$,\\w]+@)?[a-z0-9\\.\\-]+|(www\\.|[\\-;:&=\\+\\$,\\w]+@)[a-z0-9\\.\\-]+)${LINK_PATH}?)`
35 | const EMAIL = '(([^<>()\\[\\]\\\\.,;:\\s@"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))'
36 |
37 | const YT_RE = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu\.be))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/i
38 | /* eslint-enable @typescript-eslint/naming-convention */
39 |
40 | const INLINE_RULE_SET = [
41 | { regexp: /(?:(?/gim, type: MarkdownType.LINE_BREAK },
47 |
48 | { regexp: new RegExp(`!\\[(?:[^\\]]|\\\\])+]\\(${LINK}\\)`, 'img'), type: MarkdownType.IMAGE },
49 | { regexp: new RegExp(`!\\[(?:[^\\]]|\\\\])+]\\(${LINK_PATH}\\)`, 'img'), type: MarkdownType.IMAGE },
50 | { regexp: new RegExp(`!!v\\[${LINK}]`, 'img'), type: MarkdownType.VIDEO },
51 | { regexp: new RegExp(`!!v\\[${LINK_PATH}]`, 'img'), type: MarkdownType.VIDEO },
52 | { regexp: new RegExp(`\\[(?:[^\\]]|\\\\])+]\\(${LINK}\\)`, 'img'), type: MarkdownType.LINK },
53 | { regexp: new RegExp(`\\[(?:[^\\]]|\\\\])+]\\(${LINK_PATH}\\)`, 'img'), type: MarkdownType.LINK },
54 | { regexp: /\[(?:[^\]]|\\])+]\(##[\w-!./\\]*(\/[\w-!./\\]*)?(#[\w-!./\\]*)?\)/gim, type: MarkdownType.DOCUMENT },
55 | { regexp: /\[(?:[^\]]|\\])+]\(#[\w-!./\\]*\)/gim, type: MarkdownType.ANCHOR },
56 | { regexp: new RegExp(EMAIL, 'img'), type: MarkdownType.EMAIL },
57 | { regexp: new RegExp(LINK, 'img'), type: MarkdownType.LINK },
58 | ]
59 |
60 | function parseLink (node: ParsedNode): LinkMarkdownNode {
61 | if (typeof node.markup !== 'string') throw new Error('Invalid non-string node!')
62 |
63 | if (node.markup.startsWith('[')) {
64 | const exec = /\[(.+?(? formatNode(n)) }
149 | case MarkdownType.TEXT:
150 | case MarkdownType.CODE:
151 | case MarkdownType.HTTP_METHOD:
152 | case MarkdownType.HTTP_PARAM:
153 | if (typeof node.markup !== 'string') throw new Error('Invalid non-string node!')
154 | return { type: node.node, content: node.markup }
155 | case MarkdownType.EMAIL:
156 | if (typeof node.markup !== 'string') throw new Error('Invalid non-string node!')
157 | return { type: node.node, email: node.markup }
158 | case MarkdownType.LINK:
159 | case MarkdownType.ANCHOR:
160 | return parseLink(node)
161 | case MarkdownType.DOCUMENT:
162 | return parseDocument(node)
163 | case MarkdownType.IMAGE:
164 | return parseImage(node)
165 | case MarkdownType.VIDEO:
166 | return parseVideo(node)
167 | case MarkdownType.LINE_BREAK:
168 | return { type: MarkdownType.LINE_BREAK }
169 | default:
170 | throw new Error(`Unknown node ${node.node}!`)
171 | }
172 | }
173 |
174 | export default function parseInlineMarkup (markdown: string): MarkdownNode[] {
175 | const nodes = parseInline(INLINE_RULE_SET, markdown)
176 | return nodes.map((n) => formatNode(n))
177 | }
178 |
--------------------------------------------------------------------------------
/src/markdown/parser.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020-2021 Borkenware, All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | *
7 | * 1. Redistributions of source code must retain the above copyright notice, this
8 | * list of conditions and the following disclaimer.
9 | * 2. Redistributions in binary form must reproduce the above copyright notice,
10 | * this list of conditions and the following disclaimer in the
11 | * documentation and/or other materials provided with the distribution.
12 | * 3. Neither the name of the copyright holder nor the names of its contributors
13 | * may be used to endorse or promote products derived from this software without
14 | * specific prior written permission.
15 | *
16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | import { parseBlocks } from './util.js'
29 | import parseInlineMarkup from './inline.js'
30 |
31 | import { MarkdownType } from '../types/markdown.js'
32 | import type {
33 | ParsedNode, MarkdownNode, SimpleMarkdownNode, ComposedMarkdownNode, HeadingMarkdownNode,
34 | NoteMarkdownNode, ListMarkdownNode, CodeBlockMarkdownNode, HttpItemMarkdownNode,
35 | HttpMarkdownNode, ListItemMarkdownNode, TableMarkdownNode,
36 | } from '../types/markdown.js'
37 |
38 | function findTables (markdown: string): RegExpMatchArray[] {
39 | const matches = markdown.matchAll(/^(?:\|[^\n|]+)+\|\n(?:\|(?::-{2,}:| ?-{2,} ?))+\|\n(?:(?:\|[^\n|]+)+\|(?:\n|$))+/gim)
40 | const filtered = []
41 | for (const match of matches) {
42 | const pipes = match[0].split('\n').filter(Boolean)
43 | .map((l) => l.match(/(? pipes[0] === p)) filtered.push(match)
46 | }
47 | return filtered
48 | }
49 |
50 | const BLOCK_RULE_SET = [
51 | { regexp: //img, type: MarkdownType.COMMENT },
52 | { regexp: /^#{1,6} [^\n]+/gim, type: MarkdownType.HEADING },
53 | { regexp: /^[^\n]+\n[=-]{2,}/gim, type: MarkdownType.HEADING },
54 | { regexp: /^> ?(?:info|warn|danger)\n(?:(? ?[^\n]*(?:\n|$))+/gim, type: MarkdownType.NOTE },
55 | { regexp: /^(?:> ?[^\n]*(?:\n|$))+/gim, type: MarkdownType.QUOTE },
56 | { regexp: /^```[^\n]*\n(?:\n|.)+?\n```/gim, type: MarkdownType.CODE_BLOCK },
57 | { regexp: /(?:^ *(?)/g, '')
70 | .split('\n')
71 | .map((s) => s.trim())
72 | .join('\n'),
73 | }
74 | }
75 |
76 | function parseHeader (node: ParsedNode): HeadingMarkdownNode {
77 | if (typeof node.markup !== 'string') throw new Error('Invalid non-string node!')
78 |
79 | if (node.markup.startsWith('#')) {
80 | const [ h, ...title ] = node.markup.split(' ')
81 | return {
82 | type: MarkdownType.HEADING,
83 | level: h.length as 1 | 2 | 3 | 4 | 5 | 6,
84 | content: parseInlineMarkup(title.join(' ')),
85 | }
86 | }
87 |
88 | return {
89 | type: MarkdownType.HEADING,
90 | level: node.markup.endsWith('=') ? 1 : 2,
91 | content: parseInlineMarkup(node.markup.split('\n')[0]),
92 | }
93 | }
94 |
95 | function parseParagraph (node: ParsedNode): ComposedMarkdownNode {
96 | if (typeof node.markup !== 'string') throw new Error('Invalid non-string node!')
97 |
98 | return {
99 | type: MarkdownType.PARAGRAPH,
100 | content: parseInlineMarkup(node.markup),
101 | }
102 | }
103 |
104 | function parseNote (node: ParsedNode): NoteMarkdownNode {
105 | if (typeof node.markup !== 'string') throw new Error('Invalid non-string node!')
106 |
107 | const [ kind, ...inner ] = node.markup.split('\n')
108 | return {
109 | type: MarkdownType.NOTE,
110 | kind: kind.slice(1) as 'info' | 'warn' | 'danger',
111 | // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Recursive call
112 | content: parseMarkup(inner.map((l) => l.slice(2)).join('\n')),
113 | }
114 | }
115 |
116 | function parseQuote (node: ParsedNode): ComposedMarkdownNode {
117 | if (typeof node.markup !== 'string') throw new Error('Invalid non-string node!')
118 |
119 | return {
120 | type: MarkdownType.QUOTE,
121 | // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Recursive call
122 | content: parseMarkup(
123 | node.markup.split('\n')
124 | .map((l) => l.slice(2))
125 | .join('\n')
126 | ),
127 | }
128 | }
129 |
130 | function doParseList (list: string): ListMarkdownNode {
131 | const rawItems = list.split('\n').filter(Boolean)
132 | const content: Array = []
133 | const baseTab = /^ */.exec(rawItems[0])?.[0]?.length ?? 0
134 | let accumulating = false
135 | let buffer: string[] = []
136 |
137 | for (const item of rawItems) {
138 | const tab = /^ */.exec(item)?.[0]?.length ?? 0
139 | if (accumulating && tab === baseTab) {
140 | content.push(doParseList(buffer.join('\n')))
141 | accumulating = false
142 | buffer = []
143 | } else if (!accumulating && tab > baseTab) {
144 | accumulating = true
145 | }
146 |
147 | if (accumulating) buffer.push(item)
148 | else content.push({
149 | type: MarkdownType.LIST_ITEM,
150 | content: parseInlineMarkup(
151 | item.trim()
152 | .slice(2)
153 | .trim()
154 | ),
155 | })
156 | }
157 |
158 | if (buffer.length) {
159 | content.push(doParseList(buffer.join('\n')))
160 | }
161 |
162 | return {
163 | type: MarkdownType.LIST,
164 | ordered: Boolean(/^ *\d/.exec(list)),
165 | content: content,
166 | }
167 | }
168 |
169 | function parseList (node: ParsedNode): ListMarkdownNode {
170 | if (typeof node.markup !== 'string') throw new Error('Invalid non-string node!')
171 |
172 | return doParseList(node.markup)
173 | }
174 |
175 | function parseHttp (node: ParsedNode): HttpMarkdownNode {
176 | if (typeof node.markup !== 'string') throw new Error('Invalid non-string node!')
177 |
178 | const route: HttpItemMarkdownNode[] = []
179 | const exec = /^%% (?:get|post|put|patch|delete|head) ([^\n]+)/i.exec(node.markup)
180 | if (!exec) throw new Error('Invalid http node!')
181 |
182 | route.push({ type: MarkdownType.HTTP_METHOD, content: exec[1] })
183 | for (const match of exec[2].matchAll(/([^{]+)({[^}]+})?/g)) {
184 | route.push({ type: MarkdownType.TEXT, content: match[1] })
185 | if (match[2]) route.push({ type: MarkdownType.HTTP_PARAM, content: match[2] })
186 | }
187 |
188 | return {
189 | type: MarkdownType.HTTP,
190 | content: route,
191 | }
192 | }
193 |
194 | function parseCode (node: ParsedNode): CodeBlockMarkdownNode {
195 | if (typeof node.markup !== 'string') throw new Error('Invalid non-string node!')
196 |
197 | const exec = /^```([^\n]*)\n((?:.|\n)*)\n```$/i.exec(node.markup)
198 | if (!exec) throw new Error('Invalid code block node!')
199 |
200 | return {
201 | type: MarkdownType.CODE_BLOCK,
202 | language: exec[1] || null,
203 | code: exec[2],
204 | }
205 | }
206 |
207 | function parseTable (node: ParsedNode): TableMarkdownNode {
208 | if (typeof node.markup !== 'string') throw new Error('Invalid non-string node!')
209 |
210 | const [ head, align, ...rows ] = node.markup.split('\n').filter(Boolean)
211 | return {
212 | type: MarkdownType.TABLE,
213 | centered: align.split('|').slice(1, -1)
214 | .map((s) => s.includes(':')),
215 | thead: head.split('|').slice(1, -1)
216 | .map((s) => parseInlineMarkup(s.trim())),
217 | tbody: rows.map((row) => row.split('|').slice(1, -1)
218 | .map((s) => parseInlineMarkup(s.trim()))),
219 | }
220 | }
221 |
222 | function formatBlock (node: ParsedNode): MarkdownNode {
223 | switch (node.node) {
224 | case MarkdownType.COMMENT:
225 | return parseComment(node)
226 | case MarkdownType.HEADING:
227 | return parseHeader(node)
228 | case MarkdownType.PARAGRAPH:
229 | return parseParagraph(node)
230 | case MarkdownType.NOTE:
231 | return parseNote(node)
232 | case MarkdownType.QUOTE:
233 | return parseQuote(node)
234 | case MarkdownType.LIST:
235 | return parseList(node)
236 | case MarkdownType.HTTP:
237 | return parseHttp(node)
238 | case MarkdownType.CODE_BLOCK:
239 | return parseCode(node)
240 | case MarkdownType.TABLE:
241 | return parseTable(node)
242 | case MarkdownType.RULER:
243 | return { type: MarkdownType.RULER }
244 | /* istanbul ignore next */
245 | default:
246 | throw new Error(`Illegal node type encountered: ${node.node}`)
247 | }
248 | }
249 |
250 | export default function parseMarkup (markdown: string): MarkdownNode[] {
251 | const blocks = parseBlocks(BLOCK_RULE_SET, markdown)
252 | return blocks.map((b) => formatBlock(b))
253 | }
254 |
--------------------------------------------------------------------------------
/src/markdown/util.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020-2021 Borkenware, All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | *
7 | * 1. Redistributions of source code must retain the above copyright notice, this
8 | * list of conditions and the following disclaimer.
9 | * 2. Redistributions in binary form must reproduce the above copyright notice,
10 | * this list of conditions and the following disclaimer in the
11 | * documentation and/or other materials provided with the distribution.
12 | * 3. Neither the name of the copyright holder nor the names of its contributors
13 | * may be used to endorse or promote products derived from this software without
14 | * specific prior written permission.
15 | *
16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | import { MarkdownType } from '../types/markdown.js'
29 | import type { MarkdownNode, ParserBlockRule, ParserInlineRule, ParsedNode } from '../types/markdown.js'
30 |
31 | interface InlineParseMatch {
32 | start: number
33 | end: number
34 | string: string
35 | type: MarkdownType
36 | recurse: boolean
37 | }
38 |
39 | function findTextNodes (nodes: Array): string[] {
40 | const found = []
41 |
42 | for (const node of nodes) {
43 | if (typeof node === 'string') found.push(node)
44 | else if ('content' in node) {
45 | if (node.type === MarkdownType.TEXT && typeof node.content === 'string') found.push(node.content)
46 | const items = Array.isArray(node.content) ? node.content : [ node.content ]
47 | found.concat(findTextNodes(items))
48 | }
49 | }
50 |
51 | return found
52 | }
53 |
54 | export function parseBlocks (ruleset: ParserBlockRule[], markdown: string): ParsedNode[] {
55 | const buffer: Array = [ markdown ]
56 |
57 | for (const rule of ruleset) {
58 | for (let i = 0; i < buffer.length; i++) {
59 | let item: string | ParsedNode = buffer[i]
60 | if (typeof item !== 'string') continue
61 |
62 | let delta = 0
63 | const matches = typeof rule.regexp === 'function' ? rule.regexp(item) : item.matchAll(rule.regexp)
64 | for (const match of matches) {
65 | if (typeof match.index === 'undefined') continue
66 | const index = match.index - delta
67 |
68 | const before = item.slice(0, index)
69 | const after = item.slice(index + match[0].length)
70 | const block = {
71 | node: rule.type,
72 | markup: rule.noTrim ? match[0] : match[0].trim(),
73 | }
74 |
75 | const newItems = [ before, block, after ].filter(Boolean)
76 | buffer.splice(i, 1, ...newItems)
77 |
78 | delta += index + match[0].length
79 | item = buffer[i += before ? 2 : 1] as string
80 | }
81 | }
82 | }
83 |
84 | return buffer.filter((e) => typeof e !== 'string') as ParsedNode[]
85 | }
86 |
87 | export function parseInline (ruleset: ParserInlineRule[], markdown: string): ParsedNode[] {
88 | const found: InlineParseMatch[] = []
89 |
90 | for (const rule of ruleset) {
91 | for (const match of markdown.matchAll(rule.regexp)) {
92 | if (typeof match.index === 'undefined') continue
93 | found.push({
94 | start: match.index,
95 | end: match.index + match[0].length,
96 | string: match[rule.extract ?? 0],
97 | type: rule.type,
98 | recurse: Boolean(rule.recurse),
99 | })
100 | }
101 | }
102 |
103 | const sorted = found.sort((a, b) => a.start > b.start ? 1 : a.start < b.start ? -1 : 0)
104 | const res: ParsedNode[] = []
105 | let cursor = 0
106 | for (const match of sorted) {
107 | if (match.start < cursor) continue
108 | if (match.start - cursor > 0) {
109 | res.push({
110 | node: MarkdownType.TEXT,
111 | markup: markdown.slice(cursor, match.start).replace(/\n/g, ' '),
112 | })
113 | }
114 |
115 | res.push({
116 | node: match.type,
117 | markup: match.recurse ? parseInline(ruleset, match.string) : match.string,
118 | })
119 | cursor = match.end
120 | }
121 |
122 | if (cursor < markdown.length) {
123 | res.push({
124 | node: MarkdownType.TEXT,
125 | markup: markdown.slice(cursor).replace(/\n/g, ' '),
126 | })
127 | }
128 |
129 | return res
130 | }
131 |
132 | export function flattenToText (node: MarkdownNode): string | null {
133 | if (node.type === MarkdownType.TEXT) return node.content
134 | if ('content' in node && Array.isArray(node.content)) {
135 | return findTextNodes(node.content)
136 | .filter(Boolean)
137 | .join(' ')
138 | }
139 |
140 | return null
141 | }
142 |
--------------------------------------------------------------------------------
/src/types/config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020-2021 Borkenware, All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | *
7 | * 1. Redistributions of source code must retain the above copyright notice, this
8 | * list of conditions and the following disclaimer.
9 | * 2. Redistributions in binary form must reproduce the above copyright notice,
10 | * this list of conditions and the following disclaimer in the
11 | * documentation and/or other materials provided with the distribution.
12 | * 3. Neither the name of the copyright holder nor the names of its contributors
13 | * may be used to endorse or promote products derived from this software without
14 | * specific prior written permission.
15 | *
16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | export interface ConfigDocumentsFs {
29 | source: 'filesystem'
30 | assets: string
31 | path: string
32 | }
33 |
34 | export interface ConfigDocumentsRegistry {
35 | source: 'registry'
36 | assets: string
37 | path: string
38 | documents: RawDocumentRegistry
39 | }
40 |
41 | export interface RegistryCategory {
42 | category: string
43 | documents: string[]
44 | }
45 |
46 | export type RawDocumentRegistry = Array
47 |
48 | export type DocumentRegistry = {
49 | documentCount: number
50 | documents: RawDocumentRegistry
51 | }
52 |
53 | export type BuildMode = 'preact'
54 |
55 | export interface ConfigSsr {
56 | generate: boolean
57 | redirectInsecure: boolean
58 | http2: false
59 | ssl: null | {
60 | cert: string
61 | key: string
62 | }
63 | }
64 |
65 | export interface ConfigSsrH2 {
66 | generate?: boolean
67 | redirectInsecure?: boolean
68 | http2: true
69 | ssl: {
70 | cert: string
71 | key: string
72 | }
73 | }
74 |
75 | export interface Config extends Record {
76 | documents: ConfigDocumentsFs | ConfigDocumentsRegistry
77 | ui: {
78 | title: string
79 | description: string
80 | copyright: string | null
81 | logo: string | null
82 | favicon: string | null
83 | acknowledgements: boolean
84 | }
85 | build: {
86 | target: string
87 | mode: BuildMode
88 | optimizeImg: boolean
89 | offline: boolean
90 | mangle: boolean
91 | split: boolean
92 | }
93 | ssr: ConfigSsr | ConfigSsrH2
94 | }
95 |
--------------------------------------------------------------------------------
/src/types/markdown.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Borkenware, All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | *
7 | * 1. Redistributions of source code must retain the above copyright notice, this
8 | * list of conditions and the following disclaimer.
9 | * 2. Redistributions in binary form must reproduce the above copyright notice,
10 | * this list of conditions and the following disclaimer in the
11 | * documentation and/or other materials provided with the distribution.
12 | * 3. Neither the name of the copyright holder nor the names of its contributors
13 | * may be used to endorse or promote products derived from this software without
14 | * specific prior written permission.
15 | *
16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | export enum MarkdownType {
29 | // Blocks
30 | COMMENT = 'comment',
31 | HEADING = 'heading',
32 | PARAGRAPH = 'paragraph',
33 | QUOTE = 'quote',
34 | NOTE = 'note',
35 | LIST = 'list',
36 | TABLE = 'table',
37 | HTTP = 'http',
38 | CODE_BLOCK = 'code-block',
39 |
40 | // Inline
41 | TEXT = 'text',
42 | BOLD = 'bold',
43 | ITALIC = 'italic',
44 | UNDERLINE = 'underline',
45 | STRIKE_THROUGH = 'strike-through',
46 | CODE = 'code',
47 | LINK = 'link',
48 | EMAIL = 'email',
49 | ANCHOR = 'anchor',
50 | DOCUMENT = 'document',
51 | IMAGE = 'image',
52 | VIDEO = 'video',
53 | LIST_ITEM = 'list-item',
54 |
55 | // Specifics
56 | RULER = 'ruler',
57 | LINE_BREAK = 'line-break',
58 | HTTP_METHOD = 'http-method',
59 | HTTP_PARAM = 'http-param',
60 | }
61 |
62 | // Generic types
63 | export type MarkdownNode = EmptyMarkdownNode | SimpleMarkdownNode | ComposedMarkdownNode | HeadingMarkdownNode |
64 | NoteMarkdownNode | ListItemMarkdownNode | ListMarkdownNode | TableMarkdownNode | HttpItemMarkdownNode |
65 | HttpMarkdownNode | CodeBlockMarkdownNode | LinkMarkdownNode | EmailMarkdownNode | DocumentMarkdownNode |
66 | ImageMarkdownNode | VideoMarkdownNode
67 |
68 | export interface EmptyMarkdownNode {
69 | type: MarkdownType.RULER | MarkdownType.LINE_BREAK
70 | }
71 |
72 | export interface SimpleMarkdownNode {
73 | type: MarkdownType.COMMENT | MarkdownType.TEXT | MarkdownType.CODE
74 | content: string
75 | }
76 |
77 | export interface ComposedMarkdownNode {
78 | type: MarkdownType.PARAGRAPH | MarkdownType.QUOTE | MarkdownType.BOLD |
79 | MarkdownType.ITALIC | MarkdownType.UNDERLINE | MarkdownType.STRIKE_THROUGH
80 | content: MarkdownNode[]
81 | }
82 |
83 | // Block types
84 | export interface HeadingMarkdownNode {
85 | type: MarkdownType.HEADING
86 | level: 1 | 2 | 3 | 4 | 5 | 6
87 | content: MarkdownNode[]
88 | }
89 |
90 | export interface NoteMarkdownNode {
91 | type: MarkdownType.NOTE
92 | kind: 'info' | 'warn' | 'danger'
93 | content: MarkdownNode[]
94 | }
95 |
96 | export interface ListItemMarkdownNode {
97 | type: MarkdownType.LIST_ITEM
98 | content: MarkdownNode[]
99 | }
100 |
101 | export interface ListMarkdownNode {
102 | type: MarkdownType.LIST
103 | ordered: boolean
104 | content: Array
105 | }
106 |
107 | export interface TableMarkdownNode {
108 | type: MarkdownType.TABLE
109 | centered: boolean[]
110 | thead: MarkdownNode[][]
111 | tbody: MarkdownNode[][][]
112 | }
113 |
114 | export interface HttpItemMarkdownNode {
115 | type: MarkdownType.TEXT | MarkdownType.HTTP_METHOD | MarkdownType.HTTP_PARAM
116 | content: string
117 | }
118 |
119 | export interface HttpMarkdownNode {
120 | type: MarkdownType.HTTP
121 | content: HttpItemMarkdownNode[]
122 | }
123 |
124 | export interface CodeBlockMarkdownNode {
125 | type: MarkdownType.CODE_BLOCK
126 | language: string | null
127 | code: string
128 | }
129 |
130 | // Inline types
131 | export interface LinkMarkdownNode {
132 | type: MarkdownType.LINK | MarkdownType.ANCHOR
133 | href: string
134 | label: MarkdownNode[]
135 | }
136 |
137 | export interface EmailMarkdownNode {
138 | type: MarkdownType.EMAIL
139 | email: string
140 | }
141 |
142 | export interface DocumentMarkdownNode {
143 | type: MarkdownType.DOCUMENT
144 | category: string | null
145 | document: string
146 | anchor: string | null
147 | label: MarkdownNode[]
148 | }
149 |
150 | export interface ImageMarkdownNode {
151 | type: MarkdownType.IMAGE
152 | alt: string
153 | src: string
154 | }
155 |
156 | export interface VideoMarkdownNode {
157 | type: MarkdownType.VIDEO
158 | kind: 'media' | 'youtube'
159 | src: string
160 | }
161 |
162 | // Parser
163 | export interface ParserBlockRule {
164 | regexp: RegExp | ((markdown: string) => RegExpMatchArray[])
165 | type: MarkdownType
166 | noTrim?: boolean
167 | }
168 |
169 | export interface ParserInlineRule {
170 | regexp: RegExp
171 | type: MarkdownType
172 | noTrim?: boolean
173 | recurse?: boolean
174 | extract?: number
175 | }
176 |
177 | // Parser output, using different props on purpose to prevent confusion
178 | export interface ParsedNode {
179 | node: MarkdownType
180 | markup: string | ParsedNode[]
181 | }
182 |
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020-2021 Borkenware, All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | *
7 | * 1. Redistributions of source code must retain the above copyright notice, this
8 | * list of conditions and the following disclaimer.
9 | * 2. Redistributions in binary form must reproduce the above copyright notice,
10 | * this list of conditions and the following disclaimer in the
11 | * documentation and/or other materials provided with the distribution.
12 | * 3. Neither the name of the copyright holder nor the names of its contributors
13 | * may be used to endorse or promote products derived from this software without
14 | * specific prior written permission.
15 | *
16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | import { join } from 'path'
29 | import { existsSync } from 'fs'
30 | import { readdir, lstat, unlink, rmdir } from 'fs/promises'
31 |
32 | const UNITS = [ 'ns', 'µs', 'ms', 's' ]
33 |
34 | export type ExtendedType = 'string' | 'number' | 'bigint' |
35 | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' |
36 | 'array' | 'null' | 'nan'
37 |
38 | export function extendedTypeof (obj: unknown): ExtendedType {
39 | if (typeof obj === 'object' && Array.isArray(obj)) return 'array'
40 | if (typeof obj === 'object' && obj === null) return 'null'
41 | if (typeof obj === 'number' && isNaN(obj)) return 'nan'
42 |
43 | return typeof obj
44 | }
45 |
46 | export function formatDelta (from: bigint, to: bigint): string {
47 | let passes = 0
48 | let delta = Number(to - from)
49 | while (delta > 2000 && passes < 4) {
50 | delta /= 1000
51 | passes++
52 | }
53 |
54 | return `${delta.toFixed(2)} ${UNITS[passes]}`
55 | }
56 |
57 | export function sluggify (string: string): string {
58 | return string.replace(/(^\d+-|\.(md|markdown)$)/ig, '')
59 | .replace(/[^a-z]+/ig, '-')
60 | .replace(/(^-+|-+$)/ig, '')
61 | .toLowerCase()
62 | }
63 |
64 | export function slugToTitle (slug: string): string {
65 | return slug.split('-')
66 | .map((s) => s[0].toUpperCase() + s.slice(1).toLowerCase())
67 | .join(' ')
68 | }
69 |
70 | export async function rmdirRf (path: string): Promise {
71 | if (existsSync(path)) {
72 | const files = await readdir(path)
73 | for (const file of files) {
74 | const curPath = join(path, file)
75 | const stat = await lstat(curPath)
76 |
77 | if (stat.isDirectory()) {
78 | await rmdirRf(curPath)
79 | } else {
80 | await unlink(curPath)
81 | }
82 | }
83 | await rmdir(path)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "alwaysStrict": true,
5 | "strictNullChecks": true,
6 | "strictBindCallApply": true,
7 | "strictFunctionTypes": true,
8 | "strictPropertyInitialization": true,
9 | "incremental": true,
10 |
11 | "noEmitOnError": true,
12 | "noImplicitThis": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 | "noImplicitAny": true,
16 | "allowUnreachableCode": false,
17 | "allowSyntheticDefaultImports": true,
18 | "allowJs": false,
19 |
20 | "jsx": "react",
21 | "jsxFactory": "h",
22 | "jsxFragmentFactory": "Fragment",
23 | "lib": [ "es2020" ],
24 | "target": "es2020",
25 | "module": "esnext",
26 | "outDir": "./dist",
27 | "declaration": true
28 | },
29 | "include": [ "./src/**/*" ]
30 | }
31 |
--------------------------------------------------------------------------------