@thi.ng/hiccup 5.0.9 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/README.md +2 -1
- package/api.js +49 -50
- package/attribs.d.ts +13 -8
- package/attribs.js +28 -55
- package/css.js +14 -11
- package/deref.js +13 -21
- package/normalize.js +19 -16
- package/package.json +11 -9
- package/prefix.js +7 -17
- package/serialize.js +84 -232
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
- **Last updated**: 2023-12-
|
|
3
|
+
- **Last updated**: 2023-12-11T10:07:09Z
|
|
4
4
|
- **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
|
|
5
5
|
|
|
6
6
|
All notable changes to this project will be documented in this file.
|
|
@@ -9,6 +9,18 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
|
|
|
9
9
|
**Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
|
|
10
10
|
and/or version bumps of transitive dependencies.
|
|
11
11
|
|
|
12
|
+
## [5.1.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/hiccup@5.1.0) (2023-12-09)
|
|
13
|
+
|
|
14
|
+
#### 🚀 Features
|
|
15
|
+
|
|
16
|
+
- allow `class` attrib as string array ([448edd0](https://github.com/thi-ng/umbrella/commit/448edd0))
|
|
17
|
+
- update mergeClasses()
|
|
18
|
+
- update docs & tests
|
|
19
|
+
|
|
20
|
+
#### 🩹 Bug fixes
|
|
21
|
+
|
|
22
|
+
- fix mergeClasses(), add tests ([c734794](https://github.com/thi-ng/umbrella/commit/c734794))
|
|
23
|
+
|
|
12
24
|
### [5.0.6](https://github.com/thi-ng/umbrella/tree/@thi.ng/hiccup@5.0.6) (2023-11-09)
|
|
13
25
|
|
|
14
26
|
#### ♻️ Refactoring
|
package/README.md
CHANGED
|
@@ -173,7 +173,7 @@ For Node.js REPL:
|
|
|
173
173
|
const hiccup = await import("@thi.ng/hiccup");
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
-
Package sizes (brotli'd, pre-treeshake): ESM: 2.
|
|
176
|
+
Package sizes (brotli'd, pre-treeshake): ESM: 2.17 KB
|
|
177
177
|
|
|
178
178
|
## Dependencies
|
|
179
179
|
|
|
@@ -198,6 +198,7 @@ directory are using this package:
|
|
|
198
198
|
| <img src="https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/examples/markdown-parser.jpg" width="240"/> | Markdown to Hiccup to HTML parser / transformer | [Demo](https://demo.thi.ng/umbrella/markdown/) | [Source](https://github.com/thi-ng/umbrella/tree/develop/examples/markdown) |
|
|
199
199
|
| <img src="https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/examples/package-stats.png" width="240"/> | CLI util to visualize umbrella pkg stats | | [Source](https://github.com/thi-ng/umbrella/tree/develop/examples/package-stats) |
|
|
200
200
|
| <img src="https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/examples/pointfree-svg.png" width="240"/> | Generate SVG using pointfree DSL | | [Source](https://github.com/thi-ng/umbrella/tree/develop/examples/pointfree-svg) |
|
|
201
|
+
| <img src="https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/examples/rdom-formgen.jpg" width="240"/> | Basic usage of the declarative rdom-forms generator | [Demo](https://demo.thi.ng/umbrella/rdom-formgen/) | [Source](https://github.com/thi-ng/umbrella/tree/develop/examples/rdom-formgen) |
|
|
201
202
|
| <img src="https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/examples/rstream-grid.jpg" width="240"/> | Interactive grid generator, SVG generation & export, undo/redo support | [Demo](https://demo.thi.ng/umbrella/rstream-grid/) | [Source](https://github.com/thi-ng/umbrella/tree/develop/examples/rstream-grid) |
|
|
202
203
|
|
|
203
204
|
## API
|
package/api.js
CHANGED
|
@@ -1,53 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"!ATTLIST": ">\n",
|
|
1
|
+
const PROC_TAGS = {
|
|
2
|
+
"?xml": "?>\n",
|
|
3
|
+
"!DOCTYPE": ">\n",
|
|
4
|
+
"!ENTITY": ">\n",
|
|
5
|
+
"!ELEMENT": ">\n",
|
|
6
|
+
"!ATTLIST": ">\n"
|
|
8
7
|
};
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export const XML_PROC = ["?xml", { version: "1.0", encoding: "UTF-8" }];
|
|
24
|
-
/**
|
|
25
|
-
* `<!DOCTYPE html>` in hiccup format
|
|
26
|
-
*/
|
|
27
|
-
export const DOCTYPE_HTML = [DOCTYPE, "html"];
|
|
28
|
-
/** @internal */
|
|
29
|
-
export const NO_SPANS = {
|
|
30
|
-
button: 1,
|
|
31
|
-
option: 1,
|
|
32
|
-
script: 1,
|
|
33
|
-
style: 1,
|
|
34
|
-
text: 1,
|
|
35
|
-
textarea: 1,
|
|
36
|
-
title: 1,
|
|
8
|
+
const RE_TAG = /^([^\s\.#]+)(?:#([^\s\.#]+))?(?:\.([^\s#]+))?$/;
|
|
9
|
+
const COMMENT = "__COMMENT__";
|
|
10
|
+
const CDATA = "!CDATA";
|
|
11
|
+
const DOCTYPE = "!DOCTYPE";
|
|
12
|
+
const XML_PROC = ["?xml", { version: "1.0", encoding: "UTF-8" }];
|
|
13
|
+
const DOCTYPE_HTML = [DOCTYPE, "html"];
|
|
14
|
+
const NO_SPANS = {
|
|
15
|
+
button: 1,
|
|
16
|
+
option: 1,
|
|
17
|
+
script: 1,
|
|
18
|
+
style: 1,
|
|
19
|
+
text: 1,
|
|
20
|
+
textarea: 1,
|
|
21
|
+
title: 1
|
|
37
22
|
};
|
|
38
|
-
const tagMap = (tags) => tags.split(" ").reduce((acc, x) => (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
23
|
+
const tagMap = (tags) => tags.split(" ").reduce((acc, x) => (acc[x] = true, acc), {});
|
|
24
|
+
const SVG_TAGS = tagMap(
|
|
25
|
+
"animate animateColor animateMotion animateTransform circle clipPath color-profile defs desc discard ellipse feBlend feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight feDropShadow feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset fePointLight feSpecularLighting feSpotLight feTile feTurbulence filter font foreignObject g image line linearGradient marker mask metadata mpath path pattern polygon polyline radialGradient rect set stop style svg switch symbol text textPath title tref tspan use view"
|
|
26
|
+
);
|
|
27
|
+
const VOID_TAGS = tagMap(
|
|
28
|
+
"area base br col command embed hr img input keygen link meta param source stop track use wbr ?xml"
|
|
29
|
+
);
|
|
30
|
+
const NO_CLOSE_EMPTY = tagMap(
|
|
31
|
+
"animate circle ellipse line path polygon polyline rect"
|
|
32
|
+
);
|
|
33
|
+
const ATTRIB_JOIN_DELIMS = {
|
|
34
|
+
class: " ",
|
|
35
|
+
accept: ",",
|
|
36
|
+
sizes: ",",
|
|
37
|
+
srcset: ","
|
|
38
|
+
};
|
|
39
|
+
export {
|
|
40
|
+
ATTRIB_JOIN_DELIMS,
|
|
41
|
+
CDATA,
|
|
42
|
+
COMMENT,
|
|
43
|
+
DOCTYPE,
|
|
44
|
+
DOCTYPE_HTML,
|
|
45
|
+
NO_CLOSE_EMPTY,
|
|
46
|
+
NO_SPANS,
|
|
47
|
+
PROC_TAGS,
|
|
48
|
+
RE_TAG,
|
|
49
|
+
SVG_TAGS,
|
|
50
|
+
VOID_TAGS,
|
|
51
|
+
XML_PROC
|
|
53
52
|
};
|
package/attribs.d.ts
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Takes a space separated string of existing CSS class names and merges
|
|
3
|
-
*
|
|
4
|
-
* object of booleans or an `IDeref` evaluating to either. Returns
|
|
5
|
-
*
|
|
2
|
+
* Takes a space separated string of existing CSS class names and merges it with
|
|
3
|
+
* `val`, which is either another string or string array of class names, an
|
|
4
|
+
* object of booleans or an `IDeref` evaluating to either. Returns updated class
|
|
5
|
+
* string.
|
|
6
6
|
*
|
|
7
7
|
* @remarks
|
|
8
8
|
* If `val` evaluates to a string, it will be appended to `existing`.
|
|
9
9
|
*
|
|
10
|
-
* If `val` is an
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* If `val` is an array, it will be joined as space-separated string and
|
|
11
|
+
* concatenated to the existing one.
|
|
12
|
+
*
|
|
13
|
+
* If `val` is an object, its keys are used as class names and their values
|
|
14
|
+
* indicate if the class should be added or removed from the existing set.
|
|
13
15
|
*
|
|
14
16
|
* @example
|
|
15
17
|
* ```ts
|
|
16
18
|
* mergeClasses("foo bar", { foo: false, baz: true })
|
|
17
19
|
* // "bar baz"
|
|
18
20
|
*
|
|
21
|
+
* mergeClasses("foo", ["bar", "baz"]);
|
|
22
|
+
* // "foo bar baz"
|
|
23
|
+
*
|
|
19
24
|
* mergeClasses("foo bar", "baz");
|
|
20
|
-
* // "baz"
|
|
25
|
+
* // "foo bar baz"
|
|
21
26
|
* ```
|
|
22
27
|
*
|
|
23
28
|
* @param existing -
|
package/attribs.js
CHANGED
|
@@ -1,59 +1,32 @@
|
|
|
1
1
|
import { deref } from "@thi.ng/api/deref";
|
|
2
|
+
import { isArray } from "@thi.ng/checks/is-array";
|
|
2
3
|
import { isString } from "@thi.ng/checks/is-string";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* @example
|
|
17
|
-
* ```ts
|
|
18
|
-
* mergeClasses("foo bar", { foo: false, baz: true })
|
|
19
|
-
* // "bar baz"
|
|
20
|
-
*
|
|
21
|
-
* mergeClasses("foo bar", "baz");
|
|
22
|
-
* // "baz"
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* @param existing -
|
|
26
|
-
* @param val -
|
|
27
|
-
*/
|
|
28
|
-
export const mergeClasses = (existing, val) => {
|
|
29
|
-
val = deref(val);
|
|
30
|
-
if (val == null)
|
|
31
|
-
return existing;
|
|
32
|
-
if (isString(val))
|
|
33
|
-
return existing + " " + val;
|
|
34
|
-
const classes = new Set(existing.split(" "));
|
|
35
|
-
for (let id in val) {
|
|
36
|
-
deref(val[id]) ? classes.add(id) : classes.delete(id);
|
|
37
|
-
}
|
|
38
|
-
return [...classes].join(" ");
|
|
4
|
+
const mergeClasses = (existing, val) => {
|
|
5
|
+
val = deref(val);
|
|
6
|
+
if (val == null)
|
|
7
|
+
return existing;
|
|
8
|
+
if (isArray(val))
|
|
9
|
+
val = val.join(" ");
|
|
10
|
+
if (isString(val))
|
|
11
|
+
return existing ? existing + " " + val : val;
|
|
12
|
+
const classes = new Set(existing ? existing.split(" ") : void 0);
|
|
13
|
+
for (let id in val) {
|
|
14
|
+
deref(val[id]) ? classes.add(id) : classes.delete(id);
|
|
15
|
+
}
|
|
16
|
+
return [...classes].join(" ");
|
|
39
17
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
else if (aclass) {
|
|
56
|
-
attribs.class = isString(aclass) ? aclass : mergeClasses("", aclass);
|
|
57
|
-
}
|
|
58
|
-
return attribs;
|
|
18
|
+
const mergeEmmetAttribs = (attribs, id, classes) => {
|
|
19
|
+
id && (attribs.id = id);
|
|
20
|
+
let aclass = deref(attribs.class);
|
|
21
|
+
if (classes) {
|
|
22
|
+
classes = classes.replace(/\./g, " ");
|
|
23
|
+
attribs.class = aclass ? mergeClasses(classes, aclass) : classes;
|
|
24
|
+
} else if (aclass) {
|
|
25
|
+
attribs.class = isString(aclass) ? aclass : mergeClasses("", aclass);
|
|
26
|
+
}
|
|
27
|
+
return attribs;
|
|
28
|
+
};
|
|
29
|
+
export {
|
|
30
|
+
mergeClasses,
|
|
31
|
+
mergeEmmetAttribs
|
|
59
32
|
};
|
package/css.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { deref } from "@thi.ng/api/deref";
|
|
2
2
|
import { isFunction } from "@thi.ng/checks/is-function";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
const css = (rules) => {
|
|
4
|
+
let css2 = "";
|
|
5
|
+
let v;
|
|
6
|
+
for (let r in rules) {
|
|
7
|
+
v = deref(rules[r]);
|
|
8
|
+
if (isFunction(v))
|
|
9
|
+
v = v(rules);
|
|
10
|
+
if (v != null)
|
|
11
|
+
css2 += `${r}:${v};`;
|
|
12
|
+
}
|
|
13
|
+
return css2;
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
css
|
|
14
17
|
};
|
package/deref.js
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
import { implementsFunction } from "@thi.ng/checks/implements-function";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
return ctx;
|
|
16
|
-
const res = { ...ctx };
|
|
17
|
-
for (let k of keys) {
|
|
18
|
-
const v = res[k];
|
|
19
|
-
if (implementsFunction(v, "deref"))
|
|
20
|
-
res[k] = v.deref();
|
|
21
|
-
}
|
|
22
|
-
return res;
|
|
2
|
+
const derefContext = (ctx, keys) => {
|
|
3
|
+
if (ctx == null || !keys || !keys.length)
|
|
4
|
+
return ctx;
|
|
5
|
+
const res = { ...ctx };
|
|
6
|
+
for (let k of keys) {
|
|
7
|
+
const v = res[k];
|
|
8
|
+
if (implementsFunction(v, "deref"))
|
|
9
|
+
res[k] = v.deref();
|
|
10
|
+
}
|
|
11
|
+
return res;
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
derefContext
|
|
23
15
|
};
|
package/normalize.js
CHANGED
|
@@ -3,21 +3,24 @@ import { isString } from "@thi.ng/checks/is-string";
|
|
|
3
3
|
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
|
|
4
4
|
import { RE_TAG } from "./api.js";
|
|
5
5
|
import { mergeEmmetAttribs } from "./attribs.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
const normalize = (tag) => {
|
|
7
|
+
let name = tag[0];
|
|
8
|
+
let match;
|
|
9
|
+
const hasAttribs = isPlainObject(tag[1]);
|
|
10
|
+
const attribs = hasAttribs ? { ...tag[1] } : {};
|
|
11
|
+
if (!isString(name) || !(match = RE_TAG.exec(name))) {
|
|
12
|
+
illegalArgs(`"${name}" is not a valid tag name`);
|
|
13
|
+
}
|
|
14
|
+
name = match[1];
|
|
15
|
+
mergeEmmetAttribs(attribs, match[2], match[3]);
|
|
16
|
+
if (tag.length > 1) {
|
|
17
|
+
tag = tag.slice(hasAttribs ? 2 : 1).filter((x) => x != null);
|
|
18
|
+
if (tag.length > 0) {
|
|
19
|
+
return [name, attribs, tag];
|
|
13
20
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return [name, attribs, tag];
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return [name, attribs];
|
|
21
|
+
}
|
|
22
|
+
return [name, attribs];
|
|
23
|
+
};
|
|
24
|
+
export {
|
|
25
|
+
normalize
|
|
23
26
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thi.ng/hiccup",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.1",
|
|
4
4
|
"description": "HTML/SVG/XML serialization of nested data structures, iterables & closures",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
"author": "Karsten Schmidt (https://thi.ng)",
|
|
25
25
|
"license": "Apache-2.0",
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "yarn
|
|
27
|
+
"build": "yarn build:esbuild && yarn build:decl",
|
|
28
|
+
"build:decl": "tsc --declaration --emitDeclarationOnly",
|
|
29
|
+
"build:esbuild": "esbuild --format=esm --platform=neutral --target=es2022 --tsconfig=tsconfig.json --outdir=. src/**/*.ts",
|
|
28
30
|
"clean": "rimraf --glob '*.js' '*.d.ts' '*.map' doc",
|
|
29
31
|
"doc": "typedoc --excludePrivate --excludeInternal --out doc src/index.ts",
|
|
30
32
|
"doc:ae": "mkdir -p .ae/doc .ae/temp && api-extractor run --local --verbose",
|
|
@@ -33,15 +35,15 @@
|
|
|
33
35
|
"test": "bun test"
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
36
|
-
"@thi.ng/api": "^8.9.
|
|
37
|
-
"@thi.ng/checks": "^3.4.
|
|
38
|
-
"@thi.ng/errors": "^2.4.
|
|
39
|
-
"@thi.ng/strings": "^3.7.
|
|
38
|
+
"@thi.ng/api": "^8.9.12",
|
|
39
|
+
"@thi.ng/checks": "^3.4.12",
|
|
40
|
+
"@thi.ng/errors": "^2.4.6",
|
|
41
|
+
"@thi.ng/strings": "^3.7.3"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
44
|
"@microsoft/api-extractor": "^7.38.3",
|
|
43
|
-
"@thi.ng/atom": "^5.2.
|
|
44
|
-
"
|
|
45
|
+
"@thi.ng/atom": "^5.2.18",
|
|
46
|
+
"esbuild": "^0.19.8",
|
|
45
47
|
"rimraf": "^5.0.5",
|
|
46
48
|
"tools": "^0.0.1",
|
|
47
49
|
"typedoc": "^0.25.4",
|
|
@@ -126,5 +128,5 @@
|
|
|
126
128
|
],
|
|
127
129
|
"year": 2016
|
|
128
130
|
},
|
|
129
|
-
"gitHead": "
|
|
131
|
+
"gitHead": "5e7bafedfc3d53bc131469a28de31dd8e5b4a3ff\n"
|
|
130
132
|
}
|
package/prefix.js
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
* formatPrefixes({ foaf, xsd })
|
|
10
|
-
* // "foaf: http://xmlns.com/foaf/0.1/ rdf: http://www.w3.org/2001/XMLSchema#"
|
|
11
|
-
* ```
|
|
12
|
-
*
|
|
13
|
-
* @param prefixes -
|
|
14
|
-
*/
|
|
15
|
-
export const formatPrefixes = (prefixes) => Object.keys(prefixes)
|
|
16
|
-
.reduce((acc, k) => (acc.push(`${k}: ${prefixes[k]}`), acc), [])
|
|
17
|
-
.join(" ");
|
|
1
|
+
const formatPrefixes = (prefixes) => Object.keys(prefixes).reduce(
|
|
2
|
+
(acc, k) => (acc.push(`${k}: ${prefixes[k]}`), acc),
|
|
3
|
+
[]
|
|
4
|
+
).join(" ");
|
|
5
|
+
export {
|
|
6
|
+
formatPrefixes
|
|
7
|
+
};
|
package/serialize.js
CHANGED
|
@@ -7,256 +7,108 @@ import { isPlainObject } from "@thi.ng/checks/is-plain-object";
|
|
|
7
7
|
import { isString } from "@thi.ng/checks/is-string";
|
|
8
8
|
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
|
|
9
9
|
import { escapeEntitiesNum } from "@thi.ng/strings/entities";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ATTRIB_JOIN_DELIMS,
|
|
12
|
+
CDATA,
|
|
13
|
+
COMMENT,
|
|
14
|
+
NO_CLOSE_EMPTY,
|
|
15
|
+
NO_SPANS,
|
|
16
|
+
PROC_TAGS,
|
|
17
|
+
VOID_TAGS
|
|
18
|
+
} from "./api.js";
|
|
11
19
|
import { css } from "./css.js";
|
|
12
20
|
import { normalize } from "./normalize.js";
|
|
13
21
|
import { formatPrefixes } from "./prefix.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* ["tag", {...}, "body", function, ...]
|
|
26
|
-
* [function, arg1, arg2, ...]
|
|
27
|
-
* [{render: (ctx,...) => [...]}, args...]
|
|
28
|
-
* iterable
|
|
29
|
-
* ```
|
|
30
|
-
*
|
|
31
|
-
* Tags can be defined in "Emmet" convention, e.g.
|
|
32
|
-
*
|
|
33
|
-
* ```js
|
|
34
|
-
* ["div#foo.bar.baz", "hi"] // <div id="foo" class="bar baz">hi</div>
|
|
35
|
-
* ```
|
|
36
|
-
*
|
|
37
|
-
* The presence of the attributes object (2nd array index) is optional. Any
|
|
38
|
-
* attribute values, incl. functions are allowed. If the latter, the function is
|
|
39
|
-
* called with the full attribs object as argument and the return value is used
|
|
40
|
-
* for the attribute. This allows for the dynamic creation of attrib values
|
|
41
|
-
* based on other attribs. The only exception to this are event attributes, i.e.
|
|
42
|
-
* attribute names starting with "on". Function values assigned to event
|
|
43
|
-
* attributes will be omitted from the output.
|
|
44
|
-
*
|
|
45
|
-
* ```js
|
|
46
|
-
* ["div#foo", { bar: (attribs) => attribs.id + "-bar" }]
|
|
47
|
-
* // <div id="foo" bar="foo-bar"></div>
|
|
48
|
-
* ```
|
|
49
|
-
*
|
|
50
|
-
* The `style` attribute can ONLY be defined as string or object.
|
|
51
|
-
*
|
|
52
|
-
* ```js
|
|
53
|
-
* ["div", { style: { color: "red", background: "#000" } }]
|
|
54
|
-
* // <div style="color:red;background:#000;"></div>
|
|
55
|
-
* ```
|
|
56
|
-
*
|
|
57
|
-
* Boolean attribs are serialized in HTML5 syntax (present or not). `null`,
|
|
58
|
-
* `undefined` or empty string attrib values are ignored.
|
|
59
|
-
*
|
|
60
|
-
* Any `null` or `undefined` array values (other than in head position) will
|
|
61
|
-
* also be removed, unless a function is in head position.
|
|
62
|
-
*
|
|
63
|
-
* A function in head position of a node acts as a mechanism for component
|
|
64
|
-
* composition & delayed execution. The function will only be executed at
|
|
65
|
-
* serialization time. In this case the optional global context object and all
|
|
66
|
-
* other elements of that node / array are passed as arguments when that
|
|
67
|
-
* function is called. The return value the function MUST be a valid new tree
|
|
68
|
-
* (or `undefined`).
|
|
69
|
-
*
|
|
70
|
-
* If the `ctx` option is given it'll be passed to each embedded component fns.
|
|
71
|
-
* Optionally call {@link derefContext} prior to {@link serialize} to auto-deref
|
|
72
|
-
* context keys with values implementing the
|
|
73
|
-
* [`IDeref`](https://docs.thi.ng/umbrella/api/interfaces/IDeref.html)
|
|
74
|
-
* interface.
|
|
75
|
-
*
|
|
76
|
-
* ```js
|
|
77
|
-
* const foo = (ctx, a, b) => ["div#" + a, ctx.foo, b];
|
|
78
|
-
*
|
|
79
|
-
* serialize([foo, "id", "body"], { ctx: { foo: { class: "black" } } })
|
|
80
|
-
* // <div id="id" class="black">body</div>
|
|
81
|
-
* ```
|
|
82
|
-
*
|
|
83
|
-
* Functions located in other positions are called ONLY with the global context
|
|
84
|
-
* arg and can return any (serializable) value (i.e. new trees, strings,
|
|
85
|
-
* numbers, iterables or any type with a suitable `.toString()`, `.toHiccup()`
|
|
86
|
-
* or `.deref()` implementation).
|
|
87
|
-
*
|
|
88
|
-
* hiccup & hdom control attributes (i.e. attrib names prefixed with `__`) will
|
|
89
|
-
* be omitted from the output. The only control attrib supported by this package
|
|
90
|
-
* is `__serialize`. If set to `false`, the entire tree branch below (and
|
|
91
|
-
* including) the element with that attrib will be excluded from the output.
|
|
92
|
-
*
|
|
93
|
-
* **See {@link SerializeOpts} for further available options.**
|
|
94
|
-
*
|
|
95
|
-
* Single or multiline comments can be included using the special `COMMENT` tag
|
|
96
|
-
* (`"__COMMENT__"`) (always WITHOUT attributes!).
|
|
97
|
-
*
|
|
98
|
-
* ```js
|
|
99
|
-
* [COMMENT, "Hello world"]
|
|
100
|
-
* // <!-- Hello world -->
|
|
101
|
-
*
|
|
102
|
-
* [COMMENT, "Hello", "world"]
|
|
103
|
-
* // <!--
|
|
104
|
-
* // Hello
|
|
105
|
-
* // world
|
|
106
|
-
* // -->
|
|
107
|
-
* ```
|
|
108
|
-
*
|
|
109
|
-
* Currently, the only processing / DTD instructions supported are:
|
|
110
|
-
*
|
|
111
|
-
* - `?xml`
|
|
112
|
-
* - `!DOCTYTPE`
|
|
113
|
-
* - `!ELEMENT`
|
|
114
|
-
* - `!ENTITY`
|
|
115
|
-
* - `!ATTLIST`
|
|
116
|
-
*
|
|
117
|
-
* These are used as follows (attribs are only allowed for `?xml`, all others
|
|
118
|
-
* only accept a body string which is taken as is):
|
|
119
|
-
*
|
|
120
|
-
* ```js
|
|
121
|
-
* serialize(["?xml", { version: "1.0", standalone: "yes" }])
|
|
122
|
-
* // <?xml version="1.0" standalone="yes"?>
|
|
123
|
-
*
|
|
124
|
-
* ["!DOCTYPE", "html"] // (also available as DOCTYPE_HTML)
|
|
125
|
-
* // <!DOCTYPE html>
|
|
126
|
-
* ```
|
|
127
|
-
*
|
|
128
|
-
* @param tree - hiccup elements / component tree
|
|
129
|
-
* @param opts - options
|
|
130
|
-
*/
|
|
131
|
-
export const serialize = (tree, opts, path = [0]) => {
|
|
132
|
-
const $opts = {
|
|
133
|
-
escape: false,
|
|
134
|
-
escapeFn: escapeEntitiesNum,
|
|
135
|
-
span: false,
|
|
136
|
-
keys: false,
|
|
137
|
-
...opts,
|
|
138
|
-
};
|
|
139
|
-
if (opts?.keys == null && $opts.span)
|
|
140
|
-
$opts.keys = true;
|
|
141
|
-
return _serialize(tree, $opts, path);
|
|
22
|
+
const serialize = (tree, opts, path = [0]) => {
|
|
23
|
+
const $opts = {
|
|
24
|
+
escape: false,
|
|
25
|
+
escapeFn: escapeEntitiesNum,
|
|
26
|
+
span: false,
|
|
27
|
+
keys: false,
|
|
28
|
+
...opts
|
|
29
|
+
};
|
|
30
|
+
if (opts?.keys == null && $opts.span)
|
|
31
|
+
$opts.keys = true;
|
|
32
|
+
return _serialize(tree, $opts, path);
|
|
142
33
|
};
|
|
143
|
-
const _serialize = (tree, opts, path) => tree == null
|
|
144
|
-
? ""
|
|
145
|
-
: Array.isArray(tree)
|
|
146
|
-
? serializeElement(tree, opts, path)
|
|
147
|
-
: isFunction(tree)
|
|
148
|
-
? _serialize(tree(opts.ctx), opts, path)
|
|
149
|
-
: implementsFunction(tree, "toHiccup")
|
|
150
|
-
? _serialize(tree.toHiccup(opts.ctx), opts, path)
|
|
151
|
-
: isDeref(tree)
|
|
152
|
-
? _serialize(tree.deref(), opts, path)
|
|
153
|
-
: isNotStringAndIterable(tree)
|
|
154
|
-
? serializeIter(tree, opts, path)
|
|
155
|
-
: ((tree = __escape(String(tree), opts)), opts.span)
|
|
156
|
-
? `<span${opts.keys ? ` key="${path.join("-")}"` : ""}>${tree}</span>`
|
|
157
|
-
: tree;
|
|
34
|
+
const _serialize = (tree, opts, path) => tree == null ? "" : Array.isArray(tree) ? serializeElement(tree, opts, path) : isFunction(tree) ? _serialize(tree(opts.ctx), opts, path) : implementsFunction(tree, "toHiccup") ? _serialize(tree.toHiccup(opts.ctx), opts, path) : isDeref(tree) ? _serialize(tree.deref(), opts, path) : isNotStringAndIterable(tree) ? serializeIter(tree, opts, path) : (tree = __escape(String(tree), opts), opts.span) ? `<span${opts.keys ? ` key="${path.join("-")}"` : ""}>${tree}</span>` : tree;
|
|
158
35
|
const serializeElement = (tree, opts, path) => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
? _serialize(tag.render.apply(null, [opts.ctx, ...tree.slice(1)]), opts, path)
|
|
166
|
-
: tag === COMMENT
|
|
167
|
-
? serializeComment(tree)
|
|
168
|
-
: tag == CDATA
|
|
169
|
-
? serializeCData(tree)
|
|
170
|
-
: isString(tag)
|
|
171
|
-
? serializeTag(tree, opts, path)
|
|
172
|
-
: isNotStringAndIterable(tree)
|
|
173
|
-
? serializeIter(tree, opts, path)
|
|
174
|
-
: illegalArgs(`invalid tree node: ${tree}`);
|
|
36
|
+
let tag = tree[0];
|
|
37
|
+
return !tree.length ? "" : isFunction(tag) ? _serialize(tag.apply(null, [opts.ctx, ...tree.slice(1)]), opts, path) : implementsFunction(tag, "render") ? _serialize(
|
|
38
|
+
tag.render.apply(null, [opts.ctx, ...tree.slice(1)]),
|
|
39
|
+
opts,
|
|
40
|
+
path
|
|
41
|
+
) : tag === COMMENT ? serializeComment(tree) : tag == CDATA ? serializeCData(tree) : isString(tag) ? serializeTag(tree, opts, path) : isNotStringAndIterable(tree) ? serializeIter(tree, opts, path) : illegalArgs(`invalid tree node: ${tree}`);
|
|
175
42
|
};
|
|
176
43
|
const serializeTag = (tree, opts, path) => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
: !VOID_TAGS[tag] && !NO_CLOSE_EMPTY[tag]
|
|
186
|
-
? `></${tag}>`
|
|
187
|
-
: PROC_TAGS[tag] || "/>";
|
|
188
|
-
return `<${tag}${serializeAttribs(attribs, opts)}${body}`;
|
|
44
|
+
tree = normalize(tree);
|
|
45
|
+
const attribs = tree[1];
|
|
46
|
+
if (attribs.__skip || attribs.__serialize === false)
|
|
47
|
+
return "";
|
|
48
|
+
opts.keys && attribs.key === void 0 && (attribs.key = path.join("-"));
|
|
49
|
+
const tag = tree[0];
|
|
50
|
+
const body = tree[2] ? serializeBody(tag, tree[2], opts, path) : !VOID_TAGS[tag] && !NO_CLOSE_EMPTY[tag] ? `></${tag}>` : PROC_TAGS[tag] || "/>";
|
|
51
|
+
return `<${tag}${serializeAttribs(attribs, opts)}${body}`;
|
|
189
52
|
};
|
|
190
53
|
const serializeAttribs = (attribs, opts) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
54
|
+
let res = "";
|
|
55
|
+
for (let a in attribs) {
|
|
56
|
+
if (a.startsWith("__"))
|
|
57
|
+
continue;
|
|
58
|
+
const v = serializeAttrib(attribs, a, deref(attribs[a]), opts);
|
|
59
|
+
v != null && (res += v);
|
|
60
|
+
}
|
|
61
|
+
return res;
|
|
199
62
|
};
|
|
200
63
|
const serializeAttrib = (attribs, a, v, opts) => {
|
|
201
|
-
|
|
202
|
-
? null
|
|
203
|
-
: isFunction(v) && (/^on\w+/.test(a) || (v = v(attribs)) == null)
|
|
204
|
-
? null
|
|
205
|
-
: v === true
|
|
206
|
-
? " " + a
|
|
207
|
-
: v === false
|
|
208
|
-
? null
|
|
209
|
-
: a === "data"
|
|
210
|
-
? serializeDataAttribs(v, opts)
|
|
211
|
-
: attribPair(a, v, opts);
|
|
64
|
+
return v == null ? null : isFunction(v) && (/^on\w+/.test(a) || (v = v(attribs)) == null) ? null : v === true ? " " + a : v === false ? null : a === "data" ? serializeDataAttribs(v, opts) : attribPair(a, v, opts);
|
|
212
65
|
};
|
|
213
66
|
const attribPair = (a, v, opts) => {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
? css(v)
|
|
217
|
-
: a === "prefix" && isPlainObject(v)
|
|
218
|
-
? formatPrefixes(v)
|
|
219
|
-
: isArray(v)
|
|
220
|
-
? v.join(ATTRIB_JOIN_DELIMS[a] || " ")
|
|
221
|
-
: v.toString();
|
|
222
|
-
return v.length ? ` ${a}="${__escape(v, opts)}"` : null;
|
|
67
|
+
v = a === "style" && isPlainObject(v) ? css(v) : a === "prefix" && isPlainObject(v) ? formatPrefixes(v) : isArray(v) ? v.join(ATTRIB_JOIN_DELIMS[a] || " ") : v.toString();
|
|
68
|
+
return v.length ? ` ${a}="${__escape(v, opts)}"` : null;
|
|
223
69
|
};
|
|
224
70
|
const serializeDataAttribs = (data, opts) => {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
71
|
+
let res = "";
|
|
72
|
+
for (let id in data) {
|
|
73
|
+
let v = deref(data[id]);
|
|
74
|
+
isFunction(v) && (v = v(data));
|
|
75
|
+
v != null && (res += ` data-${id}="${__escape(v, opts)}"`);
|
|
76
|
+
}
|
|
77
|
+
return res;
|
|
232
78
|
};
|
|
233
79
|
const serializeBody = (tag, body, opts, path) => {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
80
|
+
if (VOID_TAGS[tag]) {
|
|
81
|
+
illegalArgs(`No body allowed in tag: ${tag}`);
|
|
82
|
+
}
|
|
83
|
+
const proc = PROC_TAGS[tag];
|
|
84
|
+
let res = proc ? " " : ">";
|
|
85
|
+
if (opts.span && !proc && !NO_SPANS[tag])
|
|
86
|
+
opts = { ...opts, span: true };
|
|
87
|
+
for (let i = 0, n = body.length; i < n; i++) {
|
|
88
|
+
res += _serialize(body[i], opts, [...path, i]);
|
|
89
|
+
}
|
|
90
|
+
return res + (proc || `</${tag}>`);
|
|
245
91
|
};
|
|
246
|
-
const serializeComment = (tree) => tree.length > 2
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
92
|
+
const serializeComment = (tree) => tree.length > 2 ? `
|
|
93
|
+
<!--
|
|
94
|
+
${tree.slice(1).map((x) => " " + x).join("\n")}
|
|
95
|
+
-->
|
|
96
|
+
` : `
|
|
97
|
+
<!-- ${tree[1]} -->
|
|
98
|
+
`;
|
|
99
|
+
const serializeCData = (tree) => `<![CDATA[
|
|
100
|
+
${tree.slice(1).join("\n")}
|
|
101
|
+
]]>`;
|
|
253
102
|
const serializeIter = (iter, opts, path) => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
103
|
+
const res = [];
|
|
104
|
+
const p = path.slice(0, path.length - 1);
|
|
105
|
+
let k = 0;
|
|
106
|
+
for (let i of iter) {
|
|
107
|
+
res.push(_serialize(i, opts, [...p, k++]));
|
|
108
|
+
}
|
|
109
|
+
return res.join("");
|
|
261
110
|
};
|
|
262
111
|
const __escape = (x, opts) => opts.escape ? opts.escapeFn(x) : x;
|
|
112
|
+
export {
|
|
113
|
+
serialize
|
|
114
|
+
};
|