Refactor routing in App component to enhance navigation and improve error handling by integrating dynamic routes and updating the NotFound route.

This commit is contained in:
becarta
2025-05-23 12:43:00 +02:00
parent f40db0f5c9
commit a544759a3b
11127 changed files with 1647032 additions and 0 deletions

82
node_modules/hast-util-from-parse5/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,82 @@
import type {Position} from 'unist'
import type {VFile} from 'vfile'
export {fromParse5} from './lib/index.js'
/**
* Configuration.
*/
export interface Options {
/**
* File used to add positional info to nodes (optional).
*
* If given, the file should represent the original HTML source.
*/
file?: VFile | null | undefined
/**
* Which space the document is in (default: `'html'`).
*
* When an `<svg>` element is found in the HTML space, this package already
* automatically switches to and from the SVG space when entering and exiting
* it.
*/
space?: Space | null | undefined
/**
* Whether to add extra positional info about starting tags, closing tags,
* and attributes to elements (default: `false`).
*
* > 👉 **Note**: only used when `file` is given.
*/
verbose?: boolean | null | undefined
}
/**
* Namespace.
*/
export type Space = 'html' | 'svg'
// Register data on hast.
declare module 'hast' {
interface ElementData {
position: {
/**
* Positional info of the start tag of an element.
*
* Field added by `hast-util-from-parse5` (a utility used inside
* `rehype-parse` responsible for parsing HTML), when passing
* `verbose: true`.
*/
opening?: Position | undefined
/**
* Positional info of the end tag of an element.
*
* Field added by `hast-util-from-parse5` (a utility used inside
* `rehype-parse` responsible for parsing HTML), when passing
* `verbose: true`.
*/
closing?: Position | undefined
/**
* Positional info of the properties of an element.
*
* Field added by `hast-util-from-parse5` (a utility used inside
* `rehype-parse` responsible for parsing HTML), when passing
* `verbose: true`.
*/
properties?: Record<string, Position | undefined> | undefined
}
}
interface RootData {
/**
* Whether the document was using quirksmode.
*
* Field added by `hast-util-from-parse5` (a utility used inside
* `rehype-parse` responsible for parsing HTML).
*/
quirksMode?: boolean | undefined
}
}

2
node_modules/hast-util-from-parse5/index.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
// Note: extra types exposed from `index.d.ts`.
export {fromParse5} from './lib/index.js'

38
node_modules/hast-util-from-parse5/lib/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,38 @@
/**
* Transform a `parse5` AST to hast.
*
* @param {DefaultTreeAdapterMap['node']} tree
* `parse5` tree to transform.
* @param {Options | null | undefined} [options]
* Configuration (optional).
* @returns {Nodes}
* hast tree.
*/
export function fromParse5(tree: DefaultTreeAdapterMap["node"], options?: Options | null | undefined): Nodes;
/**
* Info passed around about the current state.
*/
export type State = {
/**
* Corresponding file.
*/
file: VFile | undefined;
/**
* Whether location info was found.
*/
location: boolean;
/**
* Current schema.
*/
schema: Schema;
/**
* Add extra positional info.
*/
verbose: boolean | undefined;
};
import type { DefaultTreeAdapterMap } from 'parse5';
import type { Options } from 'hast-util-from-parse5';
import type { Nodes } from 'hast';
import type { VFile } from 'vfile';
import type { Schema } from 'property-information';
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"AAiCA;;;;;;;;;GASG;AACH,iCAPW,qBAAqB,CAAC,MAAM,CAAC,YAE7B,OAAO,GAAG,IAAI,GAAG,SAAS,GAExB,KAAK,CAejB;;;;;;;;UA3Ca,KAAK,GAAG,SAAS;;;;cAEjB,OAAO;;;;YAEP,MAAM;;;;aAEN,OAAO,GAAG,SAAS;;2CAhBc,QAAQ;6BAI7B,uBAAuB;2BALgB,MAAM;2BAI/C,OAAO;4BAFN,sBAAsB"}

337
node_modules/hast-util-from-parse5/lib/index.js generated vendored Normal file
View File

@@ -0,0 +1,337 @@
/**
* @import {ElementData, Element, Nodes, RootContent, Root} from 'hast'
* @import {DefaultTreeAdapterMap, Token} from 'parse5'
* @import {Schema} from 'property-information'
* @import {Point, Position} from 'unist'
* @import {VFile} from 'vfile'
* @import {Options} from 'hast-util-from-parse5'
*/
/**
* @typedef State
* Info passed around about the current state.
* @property {VFile | undefined} file
* Corresponding file.
* @property {boolean} location
* Whether location info was found.
* @property {Schema} schema
* Current schema.
* @property {boolean | undefined} verbose
* Add extra positional info.
*/
import {ok as assert} from 'devlop'
import {h, s} from 'hastscript'
import {find, html, svg} from 'property-information'
import {location} from 'vfile-location'
import {webNamespaces} from 'web-namespaces'
const own = {}.hasOwnProperty
/** @type {unknown} */
// type-coverage:ignore-next-line
const proto = Object.prototype
/**
* Transform a `parse5` AST to hast.
*
* @param {DefaultTreeAdapterMap['node']} tree
* `parse5` tree to transform.
* @param {Options | null | undefined} [options]
* Configuration (optional).
* @returns {Nodes}
* hast tree.
*/
export function fromParse5(tree, options) {
const settings = options || {}
return one(
{
file: settings.file || undefined,
location: false,
schema: settings.space === 'svg' ? svg : html,
verbose: settings.verbose || false
},
tree
)
}
/**
* Transform a node.
*
* @param {State} state
* Info passed around about the current state.
* @param {DefaultTreeAdapterMap['node']} node
* p5 node.
* @returns {Nodes}
* hast node.
*/
function one(state, node) {
/** @type {Nodes} */
let result
switch (node.nodeName) {
case '#comment': {
const reference = /** @type {DefaultTreeAdapterMap['commentNode']} */ (
node
)
result = {type: 'comment', value: reference.data}
patch(state, reference, result)
return result
}
case '#document':
case '#document-fragment': {
const reference =
/** @type {DefaultTreeAdapterMap['document'] | DefaultTreeAdapterMap['documentFragment']} */ (
node
)
const quirksMode =
'mode' in reference
? reference.mode === 'quirks' || reference.mode === 'limited-quirks'
: false
result = {
type: 'root',
children: all(state, node.childNodes),
data: {quirksMode}
}
if (state.file && state.location) {
const document = String(state.file)
const loc = location(document)
const start = loc.toPoint(0)
const end = loc.toPoint(document.length)
// Always defined as we give valid input.
assert(start, 'expected `start`')
assert(end, 'expected `end`')
result.position = {start, end}
}
return result
}
case '#documentType': {
const reference = /** @type {DefaultTreeAdapterMap['documentType']} */ (
node
)
result = {type: 'doctype'}
patch(state, reference, result)
return result
}
case '#text': {
const reference = /** @type {DefaultTreeAdapterMap['textNode']} */ (node)
result = {type: 'text', value: reference.value}
patch(state, reference, result)
return result
}
// Element.
default: {
const reference = /** @type {DefaultTreeAdapterMap['element']} */ (node)
result = element(state, reference)
return result
}
}
}
/**
* Transform children.
*
* @param {State} state
* Info passed around about the current state.
* @param {Array<DefaultTreeAdapterMap['node']>} nodes
* Nodes.
* @returns {Array<RootContent>}
* hast nodes.
*/
function all(state, nodes) {
let index = -1
/** @type {Array<RootContent>} */
const results = []
while (++index < nodes.length) {
// Assume no roots in `nodes`.
const result = /** @type {RootContent} */ (one(state, nodes[index]))
results.push(result)
}
return results
}
/**
* Transform an element.
*
* @param {State} state
* Info passed around about the current state.
* @param {DefaultTreeAdapterMap['element']} node
* `parse5` node to transform.
* @returns {Element}
* hast node.
*/
function element(state, node) {
const schema = state.schema
state.schema = node.namespaceURI === webNamespaces.svg ? svg : html
// Props.
let index = -1
/** @type {Record<string, string>} */
const properties = {}
while (++index < node.attrs.length) {
const attribute = node.attrs[index]
const name =
(attribute.prefix ? attribute.prefix + ':' : '') + attribute.name
if (!own.call(proto, name)) {
properties[name] = attribute.value
}
}
// Build.
const x = state.schema.space === 'svg' ? s : h
const result = x(node.tagName, properties, all(state, node.childNodes))
patch(state, node, result)
// Switch content.
if (result.tagName === 'template') {
const reference = /** @type {DefaultTreeAdapterMap['template']} */ (node)
const pos = reference.sourceCodeLocation
const startTag = pos && pos.startTag && position(pos.startTag)
const endTag = pos && pos.endTag && position(pos.endTag)
// Root in, root out.
const content = /** @type {Root} */ (one(state, reference.content))
if (startTag && endTag && state.file) {
content.position = {start: startTag.end, end: endTag.start}
}
result.content = content
}
state.schema = schema
return result
}
/**
* Patch positional info from `from` onto `to`.
*
* @param {State} state
* Info passed around about the current state.
* @param {DefaultTreeAdapterMap['node']} from
* p5 node.
* @param {Nodes} to
* hast node.
* @returns {undefined}
* Nothing.
*/
function patch(state, from, to) {
if ('sourceCodeLocation' in from && from.sourceCodeLocation && state.file) {
const position = createLocation(state, to, from.sourceCodeLocation)
if (position) {
state.location = true
to.position = position
}
}
}
/**
* Create clean positional information.
*
* @param {State} state
* Info passed around about the current state.
* @param {Nodes} node
* hast node.
* @param {Token.ElementLocation} location
* p5 location info.
* @returns {Position | undefined}
* Position, or nothing.
*/
function createLocation(state, node, location) {
const result = position(location)
if (node.type === 'element') {
const tail = node.children[node.children.length - 1]
// Bug for unclosed with children.
// See: <https://github.com/inikulin/parse5/issues/109>.
if (
result &&
!location.endTag &&
tail &&
tail.position &&
tail.position.end
) {
result.end = Object.assign({}, tail.position.end)
}
if (state.verbose) {
/** @type {Record<string, Position | undefined>} */
const properties = {}
/** @type {string} */
let key
if (location.attrs) {
for (key in location.attrs) {
if (own.call(location.attrs, key)) {
properties[find(state.schema, key).property] = position(
location.attrs[key]
)
}
}
}
assert(location.startTag, 'a start tag should exist')
const opening = position(location.startTag)
const closing = location.endTag ? position(location.endTag) : undefined
/** @type {ElementData['position']} */
const data = {opening}
if (closing) data.closing = closing
data.properties = properties
node.data = {position: data}
}
}
return result
}
/**
* Turn a p5 location into a position.
*
* @param {Token.Location} loc
* Location.
* @returns {Position | undefined}
* Position or nothing.
*/
function position(loc) {
const start = point({
line: loc.startLine,
column: loc.startCol,
offset: loc.startOffset
})
const end = point({
line: loc.endLine,
column: loc.endCol,
offset: loc.endOffset
})
// @ts-expect-error: we do use `undefined` for points if one or the other
// exists.
return start || end ? {start, end} : undefined
}
/**
* Filter out invalid points.
*
* @param {Point} point
* Point with potentially `undefined` values.
* @returns {Point | undefined}
* Point or nothing.
*/
function point(point) {
return point.line && point.column ? point : undefined
}

22
node_modules/hast-util-from-parse5/license generated vendored Normal file
View File

@@ -0,0 +1,22 @@
(The MIT License)
Copyright (c) Titus Wormer <tituswormer@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

120
node_modules/hast-util-from-parse5/package.json generated vendored Normal file
View File

@@ -0,0 +1,120 @@
{
"author": "Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)",
"bugs": "https://github.com/syntax-tree/hast-util-from-parse5/issues",
"contributors": [
"Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)"
],
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"devlop": "^1.0.0",
"hastscript": "^9.0.0",
"property-information": "^7.0.0",
"vfile": "^6.0.0",
"vfile-location": "^5.0.0",
"web-namespaces": "^2.0.0"
},
"description": "hast utility to transform from a `parse5` AST",
"devDependencies": {
"@types/node": "^22.0.0",
"c8": "^10.0.0",
"is-hidden": "^2.0.0",
"parse5": "^7.0.0",
"prettier": "^3.0.0",
"remark-cli": "^12.0.0",
"remark-preset-wooorm": "^11.0.0",
"to-vfile": "^8.0.0",
"type-coverage": "^2.0.0",
"typescript": "^5.0.0",
"unist-util-visit": "^5.0.0",
"xo": "^0.60.0"
},
"exports": "./index.js",
"files": [
"index.d.ts",
"index.js",
"lib/"
],
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
},
"keywords": [
"ast",
"change",
"hast-util",
"hast",
"transform",
"unist",
"utility",
"util"
],
"license": "MIT",
"name": "hast-util-from-parse5",
"prettier": {
"bracketSpacing": false,
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false
},
"remarkConfig": {
"plugins": [
"remark-preset-wooorm"
]
},
"repository": "syntax-tree/hast-util-from-parse5",
"scripts": {
"build": "tsc --build --clean && tsc --build && type-coverage",
"format": "remark --frail --quiet --output -- . && prettier --log-level warn --write -- . && xo --fix",
"test-api": "node --conditions development test/index.js",
"test-coverage": "c8 --100 --reporter lcov -- npm run test-api",
"test": "npm run build && npm run format && npm run test-coverage"
},
"sideEffects": false,
"typeCoverage": {
"atLeast": 100,
"strict": true
},
"type": "module",
"version": "8.0.3",
"xo": {
"overrides": [
{
"files": [
"**/*.d.ts"
],
"rules": {
"@typescript-eslint/array-type": [
"error",
{
"default": "generic"
}
],
"@typescript-eslint/ban-types": [
"error",
{
"extendDefaults": true
}
],
"@typescript-eslint/consistent-type-definitions": [
"error",
"interface"
]
}
},
{
"files": "test/**/*.js",
"rules": {
"no-await-in-loop": "off"
}
}
],
"prettier": true,
"rules": {
"max-depth": "off",
"unicorn/prefer-at": "off"
}
}
}

334
node_modules/hast-util-from-parse5/readme.md generated vendored Normal file
View File

@@ -0,0 +1,334 @@
# hast-util-from-parse5
[![Build][badge-build-image]][badge-build-url]
[![Coverage][badge-coverage-image]][badge-coverage-url]
[![Downloads][badge-downloads-image]][badge-downloads-url]
[![Size][badge-size-image]][badge-size-url]
[hast][github-hast] utility to transform from the
[`parse5`][github-parse5] AST.
## Contents
* [What is this?](#what-is-this)
* [When should I use this?](#when-should-i-use-this)
* [Install](#install)
* [Use](#use)
* [API](#api)
* [`fromParse5(tree[, options])`](#fromparse5tree-options)
* [`Options`](#options)
* [`Space`](#space-1)
* [Types](#types)
* [Compatibility](#compatibility)
* [Security](#security)
* [Related](#related)
* [Contribute](#contribute)
* [License](#license)
## What is this?
This package is a utility that can turn a parse5 tree into a hast tree.
## When should I use this?
You can use this package when using `parse5` as an HTML parser and wanting to
work with hast.
The utility [`hast-util-to-parse5`][github-hast-util-to-parse5] does the
inverse of this utility.
It generates `parse5`s AST again.
The utility [`hast-util-from-html`][github-hast-util-from-html] wraps this
utility and `parse5` to both parse HTML and generate hast from it.
## Install
This package is [ESM only][github-gist-esm].
In Node.js (version 16+),
install with [npm][npmjs-install]:
```sh
npm install hast-util-from-parse5
```
In Deno with [`esm.sh`][esmsh]:
```js
import {fromParse5} from "https://esm.sh/hast-util-from-parse5@8"
```
In browsers with [`esm.sh`][esmsh]:
```html
<script type="module">
import {fromParse5} from "https://esm.sh/hast-util-from-parse5@8?bundle"
</script>
```
## Use
Say our document `example.html` contains:
```html
<!doctype html><title>Hello!</title><h1 id="world">World!<!--after-->
```
…and our module `example.js` looks as follows:
```js
import {fromParse5} from 'hast-util-from-parse5'
import {parse} from 'parse5'
import {read} from 'to-vfile'
import {inspect} from 'unist-util-inspect'
const file = await read('example.html')
const p5ast = parse(String(file), {sourceCodeLocationInfo: true})
const hast = fromParse5(p5ast, {file})
console.log(inspect(hast))
```
…now running `node example.js` yields:
```text
root[2] (1:1-2:1, 0-70)
│ data: {"quirksMode":false}
├─0 doctype (1:1-1:16, 0-15)
└─1 element<html>[2]
│ properties: {}
├─0 element<head>[1]
│ │ properties: {}
│ └─0 element<title>[1] (1:16-1:37, 15-36)
│ │ properties: {}
│ └─0 text "Hello!" (1:23-1:29, 22-28)
└─1 element<body>[1]
│ properties: {}
└─0 element<h1>[3] (1:37-2:1, 36-70)
│ properties: {"id":"world"}
├─0 text "World!" (1:52-1:58, 51-57)
├─1 comment "after" (1:58-1:70, 57-69)
└─2 text "\n" (1:70-2:1, 69-70)
```
## API
This package exports the identifier [`fromParse5`][api-from-parse5].
There is no default export.
### `fromParse5(tree[, options])`
Transform a `parse5` AST to hast.
###### Parameters
* `tree`
([`Parse5Node`][github-parse5-node])
`parse5` tree to transform
* `options`
([`Options`][api-options], optional)
— configuration
###### Returns
hast tree ([`HastNode`][github-hast-nodes]).
### `Options`
Configuration (TypeScript type).
##### Fields
###### `file`
File used to add positional info to nodes
([`VFile`][github-vfile], optional).
If given,
the file should represent the original HTML source.
###### `space`
Which space the document is in
([`Space`][api-space], default: `'html'`).
When an `<svg>` element is found in the HTML space,
this package already automatically switches to and from the SVG space when
entering and exiting it.
###### `verbose`
Whether to add extra positional info about starting tags,
closing tags,
and attributes to elements
(`boolean`, default: `false`).
> 👉 **Note**:
> only used when `file` is given.
For the following HTML:
```html
<img src="http://example.com/fav.ico" alt="foo" title="bar">
```
The verbose info would looks as follows:
```js
{
type: 'element',
tagName: 'img',
properties: {src: 'http://example.com/fav.ico', alt: 'foo', title: 'bar'},
children: [],
data: {
position: {
opening: {
start: {line: 1, column: 1, offset: 0},
end: {line: 1, column: 61, offset: 60}
},
closing: null,
properties: {
src: {
start: {line: 1, column: 6, offset: 5},
end: {line: 1, column: 38, offset: 37}
},
alt: {
start: {line: 1, column: 39, offset: 38},
end: {line: 1, column: 48, offset: 47}
},
title: {
start: {line: 1, column: 49, offset: 48},
end: {line: 1, column: 60, offset: 59}
}
}
}
},
position: {
start: {line: 1, column: 1, offset: 0},
end: {line: 1, column: 61, offset: 60}
}
}
```
### `Space`
Namespace (TypeScript type).
###### Type
```ts
type Space = 'html' | 'svg'
```
## Types
This package is fully typed with [TypeScript][].
It exports the additional types [`Options`][api-options] and
[`Space`][api-space].
## Compatibility
Projects maintained by the unified collective are compatible with maintained
versions of Node.js.
When we cut a new major release,
we drop support for unmaintained versions of Node.
This means we try to keep the current release line,
`hast-util-from-parse5@8`,
compatible with Node.js 16.
## Security
Use of `hast-util-from-parse5` can open you up to a
[cross-site scripting (XSS)][wikipedia-xss] attack if Parse5s AST is unsafe.
## Related
* [`hast-util-to-parse5`][github-hast-util-to-parse5]
— transform hast to Parse5s AST
* [`hast-util-to-nlcst`](https://github.com/syntax-tree/hast-util-to-nlcst)
— transform hast to nlcst
* [`hast-util-to-mdast`](https://github.com/syntax-tree/hast-util-to-mdast)
— transform hast to mdast
* [`hast-util-to-xast`](https://github.com/syntax-tree/hast-util-to-xast)
— transform hast to xast
* [`mdast-util-to-hast`](https://github.com/syntax-tree/mdast-util-to-hast)
— transform mdast to hast
* [`mdast-util-to-nlcst`](https://github.com/syntax-tree/mdast-util-to-nlcst)
— transform mdast to nlcst
## Contribute
See [`contributing.md`][health-contributing]
in
[`syntax-tree/.github`][health]
for ways to get started.
See [`support.md`][health-support] for ways to get help.
This project has a [code of conduct][health-coc].
By interacting with this repository,
organization,
or community you agree to abide by its terms.
## License
[MIT][file-license] © [Titus Wormer][wooorm]
<!-- Definitions -->
[api-from-parse5]: #fromparse5tree-options
[api-options]: #options
[api-space]: #space-1
[badge-build-image]: https://github.com/syntax-tree/hast-util-from-parse5/workflows/main/badge.svg
[badge-build-url]: https://github.com/syntax-tree/hast-util-from-parse5/actions
[badge-coverage-image]: https://img.shields.io/codecov/c/github/syntax-tree/hast-util-from-parse5.svg
[badge-coverage-url]: https://codecov.io/github/syntax-tree/hast-util-from-parse5
[badge-downloads-image]: https://img.shields.io/npm/dm/hast-util-from-parse5.svg
[badge-downloads-url]: https://www.npmjs.com/package/hast-util-from-parse5
[badge-size-image]: https://img.shields.io/bundlejs/size/hast-util-from-parse5
[badge-size-url]: https://bundlejs.com/?q=hast-util-from-parse5
[esmsh]: https://esm.sh
[file-license]: license
[github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
[github-hast]: https://github.com/syntax-tree/hast
[github-hast-nodes]: https://github.com/syntax-tree/hast#nodes
[github-hast-util-from-html]: https://github.com/syntax-tree/hast-util-from-html
[github-hast-util-to-parse5]: https://github.com/syntax-tree/hast-util-to-parse5
[github-parse5]: https://github.com/inikulin/parse5
[github-parse5-node]: https://github.com/inikulin/parse5/blob/master/packages/parse5/lib/tree-adapters/default.ts
[github-vfile]: https://github.com/vfile/vfile
[health]: https://github.com/syntax-tree/.github
[health-coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md
[health-contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md
[health-support]: https://github.com/syntax-tree/.github/blob/main/support.md
[npmjs-install]: https://docs.npmjs.com/cli/install
[typescript]: https://www.typescriptlang.org
[wikipedia-xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
[wooorm]: https://wooorm.com