@thi.ng/hiccup 4.3.3 → 5.0.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2023-09-06T13:36:28Z
3
+ - **Last updated**: 2023-09-19T10:42:50Z
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,27 @@ 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.0.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/hiccup@5.0.0) (2023-09-19)
13
+
14
+ #### 🛑 Breaking changes
15
+
16
+ - add SerializeOpts, update serialize() ([442d777](https://github.com/thi-ng/umbrella/commit/442d777))
17
+ - BREAKING CHANGE: update serialize() args, replace with options object
18
+ - only a breaking change for "advanced" use cases
19
+ - add SerializeOpts to simplify serialize() args
20
+ - add customizable entity escaping (via new opts)
21
+ - add/update tests
22
+
23
+ #### 🩹 Bug fixes
24
+
25
+ - update entity escapes in serialize() ([369d83e](https://github.com/thi-ng/umbrella/commit/369d83e))
26
+ - use [@thi.ng/strings](https://github.com/thi-ng/umbrella/tree/main/packages/strings) escapeEntitiesNum() for better XML/SVG compatibility
27
+ - add tests
28
+
29
+ #### ♻️ Refactoring
30
+
31
+ - minor updates ([94b3de6](https://github.com/thi-ng/umbrella/commit/94b3de6))
32
+
12
33
  ## [4.3.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/hiccup@4.3.0) (2023-08-27)
13
34
 
14
35
  #### 🚀 Features
package/README.md CHANGED
@@ -36,7 +36,6 @@ This project is part of the
36
36
  - [XML / DTD processing instructions](#xml--dtd-processing-instructions)
37
37
  - [API](#api)
38
38
  - [serialize()](#serialize)
39
- - [escape()](#escape)
40
39
  - [Authors](#authors)
41
40
  - [License](#license)
42
41
 
@@ -45,34 +44,35 @@ This project is part of the
45
44
  HTML/SVG/XML serialization of nested data structures, iterables & closures.
46
45
 
47
46
  Inspired by [Hiccup](https://github.com/weavejester/hiccup) and
48
- [Reagent](http://reagent-project.github.io/) for Clojure/ClojureScript.
47
+ [Reagent](http://reagent-project.github.io/) for Clojure/ClojureScript, this
48
+ package provides key infrastructure for a number of other related libraries.
49
49
 
50
- Forget all the custom toy DSLs for templating and instead use the full
51
- power of ES6 to directly define fully data-driven, purely functional and
52
- easily *composable* components for static serialization to HTML &
53
- friends.
50
+ Forget all the custom toy DSLs for templating and instead use the full power of
51
+ modern JavaScript to directly define fully data-driven, purely functional and
52
+ easily *composable* components for static serialization to HTML & friends.
54
53
 
55
- This library is suitable for static website generation, server side rendering
54
+ This library is suitable for any SGML-style (HTML/XML/SVG/RSS/Atom etc.)
55
+ serialization, including static website/asset generation, server side rendering
56
56
  etc. For interactive use cases, please see companion packages
57
57
  [@thi.ng/rdom](https://github.com/thi-ng/umbrella/tree/develop/packages/rdom)
58
- (or the older
58
+ (or the older, now unmaintained
59
59
  [@thi.ng/hdom](https://github.com/thi-ng/umbrella/tree/develop/packages/hdom))
60
60
  and their various support packages.
61
61
 
62
62
  ### Features
63
63
 
64
- - Only uses arrays, functions, ES6 iterables / iterators / generators
64
+ - Only uses JS arrays, plain objects, functions, ES6 iterables / iterators / generators
65
65
  - Eager & lazy component composition using embedded functions / closures
66
66
  - Support for self-closing tags (incl. validation), boolean attributes
67
- - Arbitrary user context object injection for component functions
68
- - Dynamic derived attribute value generation via function values
67
+ - Arbitrary user context object injection for embedded component functions
68
+ - Dynamically derived attribute value generation via function values
69
69
  - CSS formatting of `style` attribute objects
70
- - Optional HTML entity encoding
70
+ - Optional HTML/XML entity encoding
71
71
  - Support for comments and XML/DTD processing instructions
72
- - Branch-local behavior control attributes to control serialization
72
+ - Branch-local behavior control attributes to customize serialization
73
73
  - Small (1.9KB minified) & fast
74
74
 
75
- (*) Lazy composition here means that functions are only executed at
75
+ <sup>(*)</sup> Lazy composition here means that functions are only executed at
76
76
  serialization time. Examples below...
77
77
 
78
78
  ### Use cases
@@ -86,24 +86,27 @@ serialization time. Examples below...
86
86
 
87
87
  ### No special sauce needed (or wanted)
88
88
 
89
- Using only vanilla language features simplifies the development,
90
- composability, reusability and testing of components. Furthermore, no
91
- custom template parser is required and you're only restricted by the
92
- expressiveness of the language / environment, not by your template
93
- engine.
89
+ Using only vanilla language features simplifies the development, removes need
90
+ for extra tooling, improves composability, reusability, transformation and
91
+ testing of components. No custom template parser (a la JSX or Handlebars etc.)
92
+ is required and you're only restricted by the expressiveness of the language /
93
+ environment, not by your template engine.
94
94
 
95
- Components can be defined as simple functions returning arrays or loaded
96
- via JSON/JSONP.
95
+ Components can be defined as simple arrays and/or functions returning arrays or
96
+ can be dynamically generated or loaded via JSON...
97
97
 
98
98
  ### What is Hiccup?
99
99
 
100
- For many years, [Hiccup](https://github.com/weavejester/hiccup) has been
101
- the de-facto standard to encode HTML/XML datastructures in Clojure. This
102
- library brings & extends this convention into ES6. A valid Hiccup tree
103
- is any flat (though, usually nested) array of the following possible
104
- structures. Any functions embedded in the tree are expected to return
105
- values of the same structure. Please see [examples](#examples) &
106
- [API](#api) further explanations...
100
+ For many years, [Hiccup](https://github.com/weavejester/hiccup) has been the
101
+ de-facto standard to encode HTML/XML datastructures in Clojure (and many years
102
+ before that, [the overall idea was introduced in Scheme by Oleg Kiselyov and
103
+ Kirill Lisovsky in
104
+ 1999](https://web.archive.org/web/20011225105556/http://okmij.org/ftp/Scheme/xml.html)).
105
+ This library brings & extends this convention into ES6. A valid Hiccup tree is
106
+ any flat (though, usually nested) array of the following possible structures.
107
+ Any functions embedded in the tree are expected to return values of the same
108
+ structure. Please see [examples](#examples) & [API](#api) further
109
+ explanations...
107
110
 
108
111
  ```ts
109
112
  ["tag", ...]
@@ -127,6 +130,7 @@ iterable
127
130
  - [@thi.ng/hiccup-carbon-icons](https://github.com/thi-ng/umbrella/tree/develop/packages/hiccup-carbon-icons) - Full set of IBM's Carbon icons in hiccup format
128
131
  - [@thi.ng/hiccup-css](https://github.com/thi-ng/umbrella/tree/develop/packages/hiccup-css) - CSS from nested JS data structures
129
132
  - [@thi.ng/hiccup-html](https://github.com/thi-ng/umbrella/tree/develop/packages/hiccup-html) - 100+ type-checked HTML5 element functions for [@thi.ng/hiccup](https://github.com/thi-ng/umbrella/tree/develop/packages/hiccup) related infrastructure
133
+ - [@thi.ng/hiccup-html-parse](https://github.com/thi-ng/umbrella/tree/develop/packages/hiccup-html-parse) - HTML parsing and transformation to nested JS arrays in hiccup format
130
134
  - [@thi.ng/hiccup-markdown](https://github.com/thi-ng/umbrella/tree/develop/packages/hiccup-markdown) - Markdown parser & serializer from/to Hiccup format
131
135
  - [@thi.ng/hiccup-svg](https://github.com/thi-ng/umbrella/tree/develop/packages/hiccup-svg) - SVG element functions for [@thi.ng/hiccup](https://github.com/thi-ng/umbrella/tree/develop/packages/hiccup) & related tooling
132
136
 
@@ -169,7 +173,7 @@ For Node.js REPL:
169
173
  const hiccup = await import("@thi.ng/hiccup");
170
174
  ```
171
175
 
172
- Package sizes (brotli'd, pre-treeshake): ESM: 2.09 KB
176
+ Package sizes (brotli'd, pre-treeshake): ESM: 2.14 KB
173
177
 
174
178
  ## Dependencies
175
179
 
@@ -205,7 +209,7 @@ A selection:
205
209
  ### Tags with Zencoding expansion
206
210
 
207
211
  Tag names support
208
- [Zencoding/Emmet](https://docs.emmet.io/abbreviations/syntax/#id-and-class)
212
+ [Emmet/Zencoding](https://docs.emmet.io/abbreviations/syntax/#id-and-class)
209
213
  style ID & class attribute expansion:
210
214
 
211
215
  ```ts
@@ -246,13 +250,10 @@ serialize(
246
250
  <div class="notice" selected style="background:#ff0;border:3px solid black">WARNING</div>
247
251
  ```
248
252
 
249
- If an attribute specifies a function as value, the function is called
250
- with the entire attribute object as argument. This allows for the
251
- dynamic generation of attribute values, based on existing ones. The
252
- result MUST be a string.
253
-
254
- **Function values for event attributes (any attrib name starting with
255
- "on") WILL BE OMITTED from output.**
253
+ If an attribute specifies a function as value, the function is called with the
254
+ entire attribute object as argument (incl. any `id` or `class` attribs derived
255
+ from an Emmet-style tag name). This allows for the dynamic generation of
256
+ attribute values, based on existing ones. The result MUST be a string.
256
257
 
257
258
  ```ts
258
259
  ["div#foo", { bar: (attribs) => attribs.id + "-bar" }]
@@ -262,6 +263,9 @@ result MUST be a string.
262
263
  <div id="foo" bar="foo-bar"></div>
263
264
  ```
264
265
 
266
+ Function values for event attributes (any attrib name starting with
267
+ "on") WILL BE OMITTED from output:
268
+
265
269
  ```ts
266
270
  ["div#foo", { onclick: () => alert("foo") }, "click me!"]
267
271
  ```
@@ -298,9 +302,11 @@ serialize(
298
302
 
299
303
  ### User context injection
300
304
 
301
- Every component function will receive an arbitrary user defined context
302
- object as first argument. This context object is passed to `serialize()`
303
- and is then auto-injected for every component function call.
305
+ Every component function will receive an arbitrary user defined context object
306
+ as first argument. This context object can be passed to
307
+ [`serialize()`](https://docs.thi.ng/umbrella/hiccup/functions/serialize.html)
308
+ via its [options argument]() and is then passed as arg to every component function
309
+ call.
304
310
 
305
311
  The context object should contain any global component configuration,
306
312
  e.g. for theming purposes.
@@ -321,17 +327,18 @@ const theme = {
321
327
 
322
328
  serialize(
323
329
  [section, "Hello world", "Easy theming"],
324
- { theme }
330
+ // pass context object via options
331
+ { ctx: { theme } }
325
332
  );
326
333
  // <section class="bg-black moon-gray bt b--dark-gray mt3"><h1 class="white f3">Hello world</h1>Easy theming</section>
327
334
  ```
328
335
 
329
- **Note:** Of course the context is ONLY auto-injected for lazily
330
- embedded component functions (as shown above), i.e. if the functions are
331
- wrapped in arrays and only called during serialization. If you call a
332
- component function directly, you MUST pass the context (or `null`) as
333
- first arg yourself. Likewise, if a component function doesn't make use
334
- of the context you can either:
336
+ **Note:** Of course the context is ONLY auto-injected for lazily embedded
337
+ component functions (like the examples shown above), i.e. if the functions are
338
+ wrapped in arrays and only called during serialization. If you call such a
339
+ component function directly, you MUST pass the context (or `null`) as first arg
340
+ yourself. Likewise, if a component function doesn't make use of the context you
341
+ can use either:
335
342
 
336
343
  ```ts
337
344
  // skip the context arg and require direct invocation
@@ -362,13 +369,15 @@ Also see
362
369
  [@thi.ng/hiccup-svg](https://github.com/thi-ng/umbrella/tree/develop/packages/hiccup-svg)
363
370
  for related functionality.
364
371
 
365
- ```ts
366
- const fs = require("fs");
372
+ ```ts tangle:export/readme-circles.js
373
+ import { serialize } from "@thi.ng/hiccup";
374
+ import { repeatedly } from "@thi.ng/transducers";
375
+ import { writeFileSync } from "fs";
367
376
 
368
377
  // creates an unstyled SVG circle element
369
378
  // we ignore the first arg (an auto-injected context arg)
370
379
  // context handling is described further below
371
- const circle = (_, x, y, r) => ["circle", { cx: x | 0, cy: y | 0, r: r | 0 }];
380
+ const circle = (_, x, y, r) => ["circle", { cx: ~~x, cy: ~~y, r: ~~r }];
372
381
 
373
382
  // note how this next component lazily composes `circle`.
374
383
  // This form delays evaluation of the `circle` component
@@ -382,23 +391,20 @@ const randomCircle = () => [
382
391
  Math.random() * 100
383
392
  ];
384
393
 
385
- // generator to produce iterable of `n` calls to `fn`
386
- function* repeatedly(n, fn) {
387
- while (n-- > 0) yield fn();
388
- }
389
-
390
394
  // generate 100 random circles and write serialized SVG to file
391
395
  // `randomCircle` is wrapped
392
- import { SVG_NS } from "@thi.ng/hiccup";
396
+ import { XML_SVG } from "@thi.ng/prefixes";
393
397
 
394
398
  const doc = [
395
- "svg", { xmlns: SVG_NS, width: 1000, height: 1000 },
399
+ "svg", { xmlns: XML_SVG, width: 1000, height: 1000 },
396
400
  ["g", { fill: "none", stroke: "red" },
397
- repeatedly(100, randomCircle)]];
401
+ repeatedly(randomCircle, 100)]];
398
402
 
399
- fs.writeFileSync("circles.svg", serialize(doc));
403
+ writeFileSync("export/circles.svg", serialize(doc));
400
404
  ```
401
405
 
406
+ Resulting example output:
407
+
402
408
  ```xml
403
409
  <svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000">
404
410
  <g fill="none" stroke="red">
@@ -413,7 +419,9 @@ fs.writeFileSync("circles.svg", serialize(doc));
413
419
 
414
420
  ### Data-driven component composition
415
421
 
416
- ```ts
422
+ ```js tangle:export/readme-glossary.js
423
+ import { serialize } from "@thi.ng/hiccup";
424
+
417
425
  // data
418
426
  const glossary = {
419
427
  foo: "widely used placeholder name in computing",
@@ -438,10 +446,13 @@ const widget = [
438
446
  ["h1", "Glossary"],
439
447
  [dlList, { id: "glossary" }, glossary]];
440
448
 
441
- // the 2nd arg `true` enforces HTML entity encoding (off by default)
442
- serialize(widget, null, true);
449
+ // serialize with enforced HTML entity encoding (off by default)
450
+ console.log(serialize(widget, { escape: true }));
443
451
  ```
444
452
 
453
+ (Re)formatted output (generated HTML will always be dense, without intermittent
454
+ white space):
455
+
445
456
  ```html
446
457
  <div class="widget">
447
458
  <h1>Glossary</h1>
@@ -460,7 +471,9 @@ serialize(widget, null, true);
460
471
 
461
472
  ### Stateful component
462
473
 
463
- ```ts
474
+ ```js tangle:export/readme-toc.js
475
+ import { serialize } from "@thi.ng/hiccup";
476
+
464
477
  // stateful component to create hierarchically
465
478
  // indexed & referencable section headlines:
466
479
  // e.g. "sec-1.1.2.3"
@@ -490,12 +503,16 @@ const TOC = [
490
503
  // create new indexer instance
491
504
  const section = indexer();
492
505
 
493
- serialize([
494
- "div.toc",
495
- TOC.map(([level, title]) => [section, level, title])
496
- ]);
506
+ console.log(
507
+ serialize([
508
+ "div.toc",
509
+ TOC.map(([level, title]) => [section, level, title])
510
+ ])
511
+ );
497
512
  ```
498
513
 
514
+ Re-formatted HTML output:
515
+
499
516
  ```html
500
517
  <div class="toc">
501
518
  <a name="sec-1"></a><h1>Document title</h1>
@@ -519,7 +536,7 @@ hiccup too supports such components since version 2.0.0. However, for
519
536
  static serialization only the `render` method is of interest and others
520
537
  are ignored.
521
538
 
522
- ```ts
539
+ ```js
523
540
  const component = {
524
541
  render: (ctx, title, ...body) => ["section", ["h1", title], ...body]
525
542
  };
@@ -536,8 +553,8 @@ behavior of individual elements / tree branches:
536
553
  [@thi.ng/hdom](https://github.com/thi-ng/umbrella/tree/develop/packages/hdom))
537
554
  - **`__serialize`** - if false, skips serialization (hiccup only)
538
555
 
539
- ```ts
540
- serialize(["div.container", ["div", {__skip: true}, "ignore me"]]);
556
+ ```js
557
+ serialize(["div.container", ["div", { __skip: true }, "ignore me"]]);
541
558
  // <div class="container"></div>
542
559
  ```
543
560
 
@@ -625,7 +642,7 @@ this are event attributes, i.e. attribute names starting with "on".
625
642
  The `style` attribute can ONLY be defined as string or object.
626
643
 
627
644
  ```ts
628
- ["div", {style: {color: "red", background: "#000"}}]
645
+ ["div", { style: { color: "red", background: "#000" } }]
629
646
  // <div style="color:red;background:#000;"></div>
630
647
  ```
631
648
 
@@ -645,7 +662,7 @@ new tree (or undefined).
645
662
  ```ts
646
663
  const foo = (ctx, a, b) => ["div#" + a, ctx.foo, b];
647
664
 
648
- serialize([foo, "id", "body"], { foo: { class: "black" }})
665
+ serialize([foo, "id", "body"], { foo: { class: "black" } })
649
666
  // <div id="id" class="black">body</div>
650
667
  ```
651
668
 
@@ -657,15 +674,6 @@ implementation).
657
674
  Please also see list of supported [behavior control
658
675
  attributes](#behavior-control-attributes).
659
676
 
660
- ### escape()
661
-
662
- Signature: `escape(str: string): string`
663
-
664
- Helper function. Applies HTML entity replacement on given string. If
665
- `serialize()` is called with `true` as 2nd argument, entity encoding is
666
- done automatically ([list of entities
667
- considered](https://github.com/thi-ng/umbrella/blob/develop/packages/hiccup/src/api.ts#L11)).
668
-
669
677
  ## Authors
670
678
 
671
679
  - [Karsten Schmidt](https://thi.ng)
package/api.d.ts CHANGED
@@ -8,6 +8,8 @@ export declare const RE_TAG: RegExp;
8
8
  export declare const COMMENT = "__COMMENT__";
9
9
  /** @internal */
10
10
  export declare const CDATA = "!CDATA";
11
+ /** @internal */
12
+ export declare const DOCTYPE = "!DOCTYPE";
11
13
  /**
12
14
  * XML processing instruction in hiccup format.
13
15
  *
package/api.js CHANGED
@@ -12,6 +12,8 @@ export const RE_TAG = /^([^\s\.#]+)(?:#([^\s\.#]+))?(?:\.([^\s#]+))?$/;
12
12
  export const COMMENT = "__COMMENT__";
13
13
  /** @internal */
14
14
  export const CDATA = "!CDATA";
15
+ /** @internal */
16
+ export const DOCTYPE = "!DOCTYPE";
15
17
  /**
16
18
  * XML processing instruction in hiccup format.
17
19
  *
@@ -22,7 +24,7 @@ export const XML_PROC = ["?xml", { version: "1.0", encoding: "UTF-8" }];
22
24
  /**
23
25
  * `<!DOCTYPE html>` in hiccup format
24
26
  */
25
- export const DOCTYPE_HTML = ["!DOCTYPE", "html"];
27
+ export const DOCTYPE_HTML = [DOCTYPE, "html"];
26
28
  /** @internal */
27
29
  export const NO_SPANS = {
28
30
  button: 1,
package/css.js CHANGED
@@ -5,8 +5,10 @@ export const css = (rules) => {
5
5
  let v;
6
6
  for (let r in rules) {
7
7
  v = deref(rules[r]);
8
- isFunction(v) && (v = v(rules));
9
- v != null && (css += `${r}:${v};`);
8
+ if (isFunction(v))
9
+ v = v(rules);
10
+ if (v != null)
11
+ css += `${r}:${v};`;
10
12
  }
11
13
  return css;
12
14
  };
package/deref.js CHANGED
@@ -16,7 +16,8 @@ export const derefContext = (ctx, keys) => {
16
16
  const res = { ...ctx };
17
17
  for (let k of keys) {
18
18
  const v = res[k];
19
- implementsFunction(v, "deref") && (res[k] = v.deref());
19
+ if (implementsFunction(v, "deref"))
20
+ res[k] = v.deref();
20
21
  }
21
22
  return res;
22
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/hiccup",
3
- "version": "4.3.3",
3
+ "version": "5.0.0",
4
4
  "description": "HTML/SVG/XML serialization of nested data structures, iterables & closures",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -37,7 +37,7 @@
37
37
  "@thi.ng/api": "^8.9.5",
38
38
  "@thi.ng/checks": "^3.4.5",
39
39
  "@thi.ng/errors": "^2.3.5",
40
- "@thi.ng/strings": "^3.5.0"
40
+ "@thi.ng/strings": "^3.6.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@microsoft/api-extractor": "^7.36.4",
@@ -127,5 +127,5 @@
127
127
  ],
128
128
  "year": 2016
129
129
  },
130
- "gitHead": "b2ef5a1b8932d067af4ec2fc7da03d59d6868dc7\n"
130
+ "gitHead": "c22e8996cee284ebe8ea88582beb1ab5fc6ee503\n"
131
131
  }
package/serialize.d.ts CHANGED
@@ -1,3 +1,42 @@
1
+ import type { FnU } from "@thi.ng/api";
2
+ /**
3
+ * Options to customize the behavior of {@link serialize}.
4
+ */
5
+ export interface SerializeOpts {
6
+ /**
7
+ * Arbitrary user context object
8
+ */
9
+ ctx?: any;
10
+ /**
11
+ * If true, auto-escape entities via {@link SerializeOpts.escapeFn}.
12
+ *
13
+ * @defaultValue false
14
+ */
15
+ escape: boolean;
16
+ /**
17
+ * Only used if {@link SerializeOpts.escape} is enabled. Function to escape
18
+ * entities. By default uses
19
+ * [`escapeEntitiesNum()`](https://docs.thi.ng/umbrella/strings/functions/escapeEntitiesNum.html).
20
+ */
21
+ escapeFn: FnU<string>;
22
+ /**
23
+ * If true (default: false), all text content will be wrapped in `<span>`
24
+ * elements (to ensure DOM compatibility with hdom). The only elements for
25
+ * spans are never created are listed in {@link NO_SPANS}.
26
+ *
27
+ * @defaultValue false
28
+ */
29
+ span: boolean;
30
+ /**
31
+ * If true (default: false), all elements will have an autogenerated `key`
32
+ * attribute injected. If {@link SerializeOpts.span} is enabled, `keys` will
33
+ * be enabled by default too (since in this case we assume the output is
34
+ * meant to be compatible with [`thi.ng/hdom`](https://thi.ng/hdom)).
35
+ *
36
+ * @defaultValue false
37
+ */
38
+ keys: boolean;
39
+ }
1
40
  /**
2
41
  * Recursively normalizes and serializes given tree as HTML/SVG/XML string.
3
42
  * Expands any embedded component functions with their results.
@@ -15,7 +54,7 @@
15
54
  * iterable
16
55
  * ```
17
56
  *
18
- * Tags can be defined in "Zencoding" convention, e.g.
57
+ * Tags can be defined in "Emmet" convention, e.g.
19
58
  *
20
59
  * ```js
21
60
  * ["div#foo.bar.baz", "hi"] // <div id="foo" class="bar baz">hi</div>
@@ -37,7 +76,7 @@
37
76
  * The `style` attribute can ONLY be defined as string or object.
38
77
  *
39
78
  * ```js
40
- * ["div", {style: {color: "red", background: "#000"}}]
79
+ * ["div", { style: { color: "red", background: "#000" } }]
41
80
  * // <div style="color:red;background:#000;"></div>
42
81
  * ```
43
82
  *
@@ -54,7 +93,7 @@
54
93
  * function is called. The return value the function MUST be a valid new tree
55
94
  * (or `undefined`).
56
95
  *
57
- * If the `ctx` object it'll be passed to each embedded component fns.
96
+ * If the `ctx` option is given it'll be passed to each embedded component fns.
58
97
  * Optionally call {@link derefContext} prior to {@link serialize} to auto-deref
59
98
  * context keys with values implementing the
60
99
  * [`IDeref`](https://docs.thi.ng/umbrella/api/interfaces/IDeref.html)
@@ -63,7 +102,7 @@
63
102
  * ```js
64
103
  * const foo = (ctx, a, b) => ["div#" + a, ctx.foo, b];
65
104
  *
66
- * serialize([foo, "id", "body"], { foo: { class: "black" } })
105
+ * serialize([foo, "id", "body"], { ctx: { foo: { class: "black" } } })
67
106
  * // <div id="id" class="black">body</div>
68
107
  * ```
69
108
  *
@@ -72,33 +111,25 @@
72
111
  * numbers, iterables or any type with a suitable `.toString()`, `.toHiccup()`
73
112
  * or `.deref()` implementation).
74
113
  *
75
- * If the optional `span` flag is true (default: false), all text content will
76
- * be wrapped in <span> elements (this is to ensure DOM compatibility with
77
- * hdom). The only elements for spans are never created are listed in `NO_SPANS`
78
- * in `api.ts`.
79
- *
80
- * If the optional `keys` flag is true (default: false), all elements will have
81
- * an autogenerated `key` attribute injected. If `span` is enabled, `keys` will
82
- * be enabled by default too (since in this case we assume the output is meant
83
- * to be compatible with [`thi.ng/hdom`](https://thi.ng/hdom)).
84
- *
85
114
  * hiccup & hdom control attributes (i.e. attrib names prefixed with `__`) will
86
115
  * be omitted from the output. The only control attrib supported by this package
87
- * is `__serialize`. If set to `false`, the entire tree branch will be excluded
88
- * from the output.
116
+ * is `__serialize`. If set to `false`, the entire tree branch below (and
117
+ * including) the element with that attrib will be excluded from the output.
118
+ *
119
+ * **See {@link SerializeOpts} for further available options.**
89
120
  *
90
121
  * Single or multiline comments can be included using the special `COMMENT` tag
91
- * (`__COMMENT__`) (always WITHOUT attributes!).
122
+ * (`"__COMMENT__"`) (always WITHOUT attributes!).
92
123
  *
93
- * ```
124
+ * ```js
94
125
  * [COMMENT, "Hello world"]
95
126
  * // <!-- Hello world -->
96
127
  *
97
128
  * [COMMENT, "Hello", "world"]
98
- * <!--
99
- * Hello
100
- * world
101
- * -->
129
+ * // <!--
130
+ * // Hello
131
+ * // world
132
+ * // -->
102
133
  * ```
103
134
  *
104
135
  * Currently, the only processing / DTD instructions supported are:
@@ -112,19 +143,16 @@
112
143
  * These are used as follows (attribs are only allowed for `?xml`, all others
113
144
  * only accept a body string which is taken as is):
114
145
  *
115
- * ```
116
- * ["?xml", { version: "1.0", standalone: "yes" }]
146
+ * ```js
147
+ * serialize(["?xml", { version: "1.0", standalone: "yes" }])
117
148
  * // <?xml version="1.0" standalone="yes"?>
118
149
  *
119
- * ["!DOCTYPE", "html"]
150
+ * ["!DOCTYPE", "html"] // (also available as DOCTYPE_HTML)
120
151
  * // <!DOCTYPE html>
121
152
  * ```
122
153
  *
123
154
  * @param tree - hiccup elements / component tree
124
- * @param ctx - arbitrary user context object
125
- * @param escape - auto-escape entities
126
- * @param span - use spans for text content
127
- * @param keys - attach key attribs
155
+ * @param opts - options
128
156
  */
129
- export declare const serialize: (tree: any, ctx?: any, escape?: boolean, span?: boolean, keys?: boolean, path?: number[]) => string;
157
+ export declare const serialize: (tree: any, opts?: Partial<SerializeOpts>, path?: number[]) => string;
130
158
  //# sourceMappingURL=serialize.d.ts.map
package/serialize.js CHANGED
@@ -6,7 +6,7 @@ import { isNotStringAndIterable } from "@thi.ng/checks/is-not-string-iterable";
6
6
  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
- import { escapeEntities } from "@thi.ng/strings/entities";
9
+ import { escapeEntitiesNum } from "@thi.ng/strings/entities";
10
10
  import { ATTRIB_JOIN_DELIMS, CDATA, COMMENT, NO_CLOSE_EMPTY, NO_SPANS, PROC_TAGS, VOID_TAGS, } from "./api.js";
11
11
  import { css } from "./css.js";
12
12
  import { normalize } from "./normalize.js";
@@ -28,7 +28,7 @@ import { formatPrefixes } from "./prefix.js";
28
28
  * iterable
29
29
  * ```
30
30
  *
31
- * Tags can be defined in "Zencoding" convention, e.g.
31
+ * Tags can be defined in "Emmet" convention, e.g.
32
32
  *
33
33
  * ```js
34
34
  * ["div#foo.bar.baz", "hi"] // <div id="foo" class="bar baz">hi</div>
@@ -50,7 +50,7 @@ import { formatPrefixes } from "./prefix.js";
50
50
  * The `style` attribute can ONLY be defined as string or object.
51
51
  *
52
52
  * ```js
53
- * ["div", {style: {color: "red", background: "#000"}}]
53
+ * ["div", { style: { color: "red", background: "#000" } }]
54
54
  * // <div style="color:red;background:#000;"></div>
55
55
  * ```
56
56
  *
@@ -67,7 +67,7 @@ import { formatPrefixes } from "./prefix.js";
67
67
  * function is called. The return value the function MUST be a valid new tree
68
68
  * (or `undefined`).
69
69
  *
70
- * If the `ctx` object it'll be passed to each embedded component fns.
70
+ * If the `ctx` option is given it'll be passed to each embedded component fns.
71
71
  * Optionally call {@link derefContext} prior to {@link serialize} to auto-deref
72
72
  * context keys with values implementing the
73
73
  * [`IDeref`](https://docs.thi.ng/umbrella/api/interfaces/IDeref.html)
@@ -76,7 +76,7 @@ import { formatPrefixes } from "./prefix.js";
76
76
  * ```js
77
77
  * const foo = (ctx, a, b) => ["div#" + a, ctx.foo, b];
78
78
  *
79
- * serialize([foo, "id", "body"], { foo: { class: "black" } })
79
+ * serialize([foo, "id", "body"], { ctx: { foo: { class: "black" } } })
80
80
  * // <div id="id" class="black">body</div>
81
81
  * ```
82
82
  *
@@ -85,33 +85,25 @@ import { formatPrefixes } from "./prefix.js";
85
85
  * numbers, iterables or any type with a suitable `.toString()`, `.toHiccup()`
86
86
  * or `.deref()` implementation).
87
87
  *
88
- * If the optional `span` flag is true (default: false), all text content will
89
- * be wrapped in <span> elements (this is to ensure DOM compatibility with
90
- * hdom). The only elements for spans are never created are listed in `NO_SPANS`
91
- * in `api.ts`.
92
- *
93
- * If the optional `keys` flag is true (default: false), all elements will have
94
- * an autogenerated `key` attribute injected. If `span` is enabled, `keys` will
95
- * be enabled by default too (since in this case we assume the output is meant
96
- * to be compatible with [`thi.ng/hdom`](https://thi.ng/hdom)).
97
- *
98
88
  * hiccup & hdom control attributes (i.e. attrib names prefixed with `__`) will
99
89
  * be omitted from the output. The only control attrib supported by this package
100
- * is `__serialize`. If set to `false`, the entire tree branch will be excluded
101
- * from the output.
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.**
102
94
  *
103
95
  * Single or multiline comments can be included using the special `COMMENT` tag
104
- * (`__COMMENT__`) (always WITHOUT attributes!).
96
+ * (`"__COMMENT__"`) (always WITHOUT attributes!).
105
97
  *
106
- * ```
98
+ * ```js
107
99
  * [COMMENT, "Hello world"]
108
100
  * // <!-- Hello world -->
109
101
  *
110
102
  * [COMMENT, "Hello", "world"]
111
- * <!--
112
- * Hello
113
- * world
114
- * -->
103
+ * // <!--
104
+ * // Hello
105
+ * // world
106
+ * // -->
115
107
  * ```
116
108
  *
117
109
  * Currently, the only processing / DTD instructions supported are:
@@ -125,79 +117,87 @@ import { formatPrefixes } from "./prefix.js";
125
117
  * These are used as follows (attribs are only allowed for `?xml`, all others
126
118
  * only accept a body string which is taken as is):
127
119
  *
128
- * ```
129
- * ["?xml", { version: "1.0", standalone: "yes" }]
120
+ * ```js
121
+ * serialize(["?xml", { version: "1.0", standalone: "yes" }])
130
122
  * // <?xml version="1.0" standalone="yes"?>
131
123
  *
132
- * ["!DOCTYPE", "html"]
124
+ * ["!DOCTYPE", "html"] // (also available as DOCTYPE_HTML)
133
125
  * // <!DOCTYPE html>
134
126
  * ```
135
127
  *
136
128
  * @param tree - hiccup elements / component tree
137
- * @param ctx - arbitrary user context object
138
- * @param escape - auto-escape entities
139
- * @param span - use spans for text content
140
- * @param keys - attach key attribs
129
+ * @param opts - options
141
130
  */
142
- export const serialize = (tree, ctx, escape = false, span = false, keys = span, path = [0]) => _serialize(tree, ctx, escape, span, keys, path);
143
- const _serialize = (tree, ctx, esc, span, keys, path) => tree == null
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);
142
+ };
143
+ const _serialize = (tree, opts, path) => tree == null
144
144
  ? ""
145
145
  : Array.isArray(tree)
146
- ? serializeElement(tree, ctx, esc, span, keys, path)
146
+ ? serializeElement(tree, opts, path)
147
147
  : isFunction(tree)
148
- ? _serialize(tree(ctx), ctx, esc, span, keys, path)
148
+ ? _serialize(tree(opts.ctx), opts, path)
149
149
  : implementsFunction(tree, "toHiccup")
150
- ? _serialize(tree.toHiccup(ctx), ctx, esc, span, keys, path)
150
+ ? _serialize(tree.toHiccup(opts.ctx), opts, path)
151
151
  : isDeref(tree)
152
- ? _serialize(tree.deref(), ctx, esc, span, keys, path)
152
+ ? _serialize(tree.deref(), opts, path)
153
153
  : isNotStringAndIterable(tree)
154
- ? serializeIter(tree, ctx, esc, span, keys, path)
155
- : ((tree = esc ? escapeEntities(String(tree)) : String(tree)), span)
156
- ? `<span${keys ? ` key="${path.join("-")}"` : ""}>${tree}</span>`
154
+ ? serializeIter(tree, opts, path)
155
+ : ((tree = __escape(String(tree), opts)), opts.span)
156
+ ? `<span${opts.keys ? ` key="${path.join("-")}"` : ""}>${tree}</span>`
157
157
  : tree;
158
- const serializeElement = (tree, ctx, esc, span, keys, path) => {
158
+ const serializeElement = (tree, opts, path) => {
159
159
  let tag = tree[0];
160
160
  return !tree.length
161
161
  ? ""
162
162
  : isFunction(tag)
163
- ? _serialize(tag.apply(null, [ctx, ...tree.slice(1)]), ctx, esc, span, keys, path)
163
+ ? _serialize(tag.apply(null, [opts.ctx, ...tree.slice(1)]), opts, path)
164
164
  : implementsFunction(tag, "render")
165
- ? _serialize(tag.render.apply(null, [ctx, ...tree.slice(1)]), ctx, esc, span, keys, path)
165
+ ? _serialize(tag.render.apply(null, [opts.ctx, ...tree.slice(1)]), opts, path)
166
166
  : tag === COMMENT
167
167
  ? serializeComment(tree)
168
168
  : tag == CDATA
169
169
  ? serializeCData(tree)
170
170
  : isString(tag)
171
- ? serializeTag(tree, ctx, esc, span, keys, path)
171
+ ? serializeTag(tree, opts, path)
172
172
  : isNotStringAndIterable(tree)
173
- ? serializeIter(tree, ctx, esc, span, keys, path)
173
+ ? serializeIter(tree, opts, path)
174
174
  : illegalArgs(`invalid tree node: ${tree}`);
175
175
  };
176
- const serializeTag = (tree, ctx, esc, span, keys, path) => {
176
+ const serializeTag = (tree, opts, path) => {
177
177
  tree = normalize(tree);
178
178
  const attribs = tree[1];
179
179
  if (attribs.__skip || attribs.__serialize === false)
180
180
  return "";
181
- keys && attribs.key === undefined && (attribs.key = path.join("-"));
181
+ opts.keys && attribs.key === undefined && (attribs.key = path.join("-"));
182
182
  const tag = tree[0];
183
183
  const body = tree[2]
184
- ? serializeBody(tag, tree[2], ctx, esc, span, keys, path)
184
+ ? serializeBody(tag, tree[2], opts, path)
185
185
  : !VOID_TAGS[tag] && !NO_CLOSE_EMPTY[tag]
186
186
  ? `></${tag}>`
187
187
  : PROC_TAGS[tag] || "/>";
188
- return `<${tag}${serializeAttribs(attribs, esc)}${body}`;
188
+ return `<${tag}${serializeAttribs(attribs, opts)}${body}`;
189
189
  };
190
- const serializeAttribs = (attribs, esc) => {
190
+ const serializeAttribs = (attribs, opts) => {
191
191
  let res = "";
192
192
  for (let a in attribs) {
193
193
  if (a.startsWith("__"))
194
194
  continue;
195
- const v = serializeAttrib(attribs, a, deref(attribs[a]), esc);
195
+ const v = serializeAttrib(attribs, a, deref(attribs[a]), opts);
196
196
  v != null && (res += v);
197
197
  }
198
198
  return res;
199
199
  };
200
- const serializeAttrib = (attribs, a, v, esc) => {
200
+ const serializeAttrib = (attribs, a, v, opts) => {
201
201
  return v == null
202
202
  ? null
203
203
  : isFunction(v) && (/^on\w+/.test(a) || (v = v(attribs)) == null)
@@ -207,10 +207,10 @@ const serializeAttrib = (attribs, a, v, esc) => {
207
207
  : v === false
208
208
  ? null
209
209
  : a === "data"
210
- ? serializeDataAttribs(v, esc)
211
- : attribPair(a, v, esc);
210
+ ? serializeDataAttribs(v, opts)
211
+ : attribPair(a, v, opts);
212
212
  };
213
- const attribPair = (a, v, esc) => {
213
+ const attribPair = (a, v, opts) => {
214
214
  v =
215
215
  a === "style" && isPlainObject(v)
216
216
  ? css(v)
@@ -219,26 +219,27 @@ const attribPair = (a, v, esc) => {
219
219
  : isArray(v)
220
220
  ? v.join(ATTRIB_JOIN_DELIMS[a] || " ")
221
221
  : v.toString();
222
- return v.length ? ` ${a}="${esc ? escapeEntities(v) : v}"` : null;
222
+ return v.length ? ` ${a}="${__escape(v, opts)}"` : null;
223
223
  };
224
- const serializeDataAttribs = (data, esc) => {
224
+ const serializeDataAttribs = (data, opts) => {
225
225
  let res = "";
226
226
  for (let id in data) {
227
227
  let v = deref(data[id]);
228
228
  isFunction(v) && (v = v(data));
229
- v != null && (res += ` data-${id}="${esc ? escapeEntities(v) : v}"`);
229
+ v != null && (res += ` data-${id}="${__escape(v, opts)}"`);
230
230
  }
231
231
  return res;
232
232
  };
233
- const serializeBody = (tag, body, ctx, esc, span, keys, path) => {
233
+ const serializeBody = (tag, body, opts, path) => {
234
234
  if (VOID_TAGS[tag]) {
235
235
  illegalArgs(`No body allowed in tag: ${tag}`);
236
236
  }
237
237
  const proc = PROC_TAGS[tag];
238
238
  let res = proc ? " " : ">";
239
- span = span && !proc && !NO_SPANS[tag];
239
+ if (opts.span && !proc && !NO_SPANS[tag])
240
+ opts = { ...opts, span: true };
240
241
  for (let i = 0, n = body.length; i < n; i++) {
241
- res += _serialize(body[i], ctx, esc, span, keys, [...path, i]);
242
+ res += _serialize(body[i], opts, [...path, i]);
242
243
  }
243
244
  return res + (proc || `</${tag}>`);
244
245
  };
@@ -249,12 +250,13 @@ const serializeComment = (tree) => tree.length > 2
249
250
  .join("\n")}\n-->\n`
250
251
  : `\n<!-- ${tree[1]} -->\n`;
251
252
  const serializeCData = (tree) => `<![CDATA[\n${tree.slice(1).join("\n")}\n]]>`;
252
- const serializeIter = (iter, ctx, esc, span, keys, path) => {
253
+ const serializeIter = (iter, opts, path) => {
253
254
  const res = [];
254
255
  const p = path.slice(0, path.length - 1);
255
256
  let k = 0;
256
257
  for (let i of iter) {
257
- res.push(_serialize(i, ctx, esc, span, keys, [...p, k++]));
258
+ res.push(_serialize(i, opts, [...p, k++]));
258
259
  }
259
260
  return res.join("");
260
261
  };
262
+ const __escape = (x, opts) => opts.escape ? opts.escapeFn(x) : x;