├── .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 | [![License](https://img.shields.io/github/license/borkenware/spoonfeed.svg?style=flat-square)](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 | ![My Beautiful Cat](/photos/cat.jpg) 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 | --------------------------------------------------------------------------------