mancha 0.10.0 → 0.12.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.
@@ -0,0 +1,41 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: read
10
+ pages: write
11
+ id-token: write
12
+
13
+ concurrency:
14
+ group: "pages"
15
+ cancel-in-progress: false
16
+
17
+ jobs:
18
+ build-test-deploy:
19
+ runs-on: ubuntu-latest
20
+ name: Build Test Deploy
21
+ environment:
22
+ name: github-pages
23
+ url: ${{ steps.deployment.outputs.page_url }}
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: actions/setup-node@v4
27
+ with:
28
+ node-version: "22"
29
+ - name: Install dependencies
30
+ run: npm install --include=dev
31
+ - name: Build project
32
+ run: npm run build
33
+ - name: Test project
34
+ run: npm run test
35
+ - name: Upload artifact
36
+ uses: actions/upload-pages-artifact@v3
37
+ with:
38
+ path: "dist"
39
+ - name: Deploy to GitHub Pages
40
+ id: deployment
41
+ uses: actions/deploy-pages@v4
package/README.md CHANGED
@@ -63,7 +63,8 @@ front-end development. Whether you decide to break up your app into reusable par
63
63
  `<include>` or create custom web components, you can write HTML as if your mother was watching.
64
64
 
65
65
  `mancha` implements its own reactivity engine, so the bundled browser module contains no external
66
- dependencies.
66
+ dependencies with the exception of [`jexpr`][jexpr] for safe expression evaluation (see the
67
+ [dependencies section](#dependencies)).
67
68
 
68
69
  ## Preprocessing
69
70
 
@@ -130,6 +131,10 @@ element tag or attributes match a specific criteria. Here's the list of attribut
130
131
  ```html
131
132
  <div :data="{foo: false}" :show="foo"></div>
132
133
  ```
134
+ - `:class` appends rendered text to existing class attribute
135
+ ```html
136
+ <span :class="error ? 'red' : 'blue'" class="text-xl">...</span>
137
+ ```
133
138
  - `:bind` binds (two-way) a variable to the `value` or `checked` property of the element.
134
139
  ```html
135
140
  <div :data="{ name: 'Stranger' }">
@@ -140,9 +145,13 @@ element tag or attributes match a specific criteria. Here's the list of attribut
140
145
  ```html
141
146
  <button :on:click="console.log('clicked')"></button>
142
147
  ```
143
- - `:{attribute}` sets the corresponding property for `attribute` in the node
148
+ - `:attr:{name}` sets the corresponding attribute for `name` in the node
149
+ ```html
150
+ <a :attr:href="buildUrl()"></a>
151
+ ```
152
+ - `:prop:{name}` sets the corresponding property for (camel-case converted) `name` in the node
144
153
  ```html
145
- <a :href="buildUrl()"></a>
154
+ <video :prop:src="buildSrc()"></video>
146
155
  ```
147
156
  - `{{ value }}` replaces `value` in text nodes
148
157
  ```html
@@ -153,7 +162,7 @@ element tag or attributes match a specific criteria. Here's the list of attribut
153
162
 
154
163
  To avoid violation of Content Security Policy (CSP) that forbids the use of `eval()`, `Mancha`
155
164
  evaluates all expressions using [`jexpr`][jexpr]. This means that only simple expressions are
156
- allowed, and spaces must be used to separate different expressions tokens. For example:
165
+ allowed, and spaces must be used to separate different expression tokens. For example:
157
166
 
158
167
  ```html
159
168
  <!-- Valid expression: string concatenation -->
@@ -210,13 +219,13 @@ allowed, and spaces must be used to separate different expressions tokens. For e
210
219
  </body>
211
220
  ```
212
221
 
213
- ## Scoping
222
+ ## Variable Scoping
214
223
 
215
224
  Contents of the `:data` attribute are only available to subnodes in the HTML tree. This is better
216
225
  illustrated with an example:
217
226
 
218
227
  ```html
219
- <body :data="{ name: 'stranger' }">
228
+ <body :data="{ name: 'stranger', key: '1234' }">
220
229
  <!-- Hello, stranger -->
221
230
  <h1>Hello, {{ name }}</h1>
222
231
 
@@ -225,7 +234,7 @@ illustrated with an example:
225
234
 
226
235
  <!-- How are you, danger? The secret message is "secret" -->
227
236
  <p :data="{ name: 'danger', message: 'secret' }">
228
- How are you, {{ name }}? The secret message is: "{{ message }}".
237
+ How are you, {{ name }}? The secret message is: "{{ message }}"".
229
238
  </p>
230
239
  </body>
231
240
  ```
@@ -257,6 +266,19 @@ const subrenderer = document.querySelector("p").renderer;
257
266
  subrenderer.$.message = "banana";
258
267
  ```
259
268
 
269
+ To access variables defined in the parent renderer, you can use the subrenderer's `$parent`
270
+ attribute:
271
+
272
+ ```html
273
+ <body :data="{ name: 'stranger' }">
274
+ <!-- Hello, stranger! -->
275
+ <p :data="{}">Hello, {{ $parent.name }}!</p>
276
+ </body>
277
+ ```
278
+
279
+ Renderers also have a `$root` attribute, which references the root element where `mancha` was
280
+ mounted and defaults to the document's body, unless explicitly provided.
281
+
260
282
  ## Styling
261
283
 
262
284
  Some basic styling rules are built into the library and can be optionally used. The styling
@@ -264,7 +286,7 @@ component was designed to be used in the browser, and it's enabled by adding a `
264
286
  to the `<script>` tag that loads `mancha`. The supported rulesets are:
265
287
 
266
288
  - `basic`: inspired by [these rules](https://www.swyx.io/css-100-bytes), the full CSS can be found
267
- [here](./src/css_raw_basic.css).
289
+ [here](./src/css_gen_basic.ts).
268
290
  - `utils`: utility classes inspired by [tailwindcss](https://tailwindcss.com), the resulting CSS is
269
291
  a drop-in replacement for a subset of the classes provided by `tailwindcss` with the main
270
292
  exception of the color palette which is borrowed from
package/dist/browser.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { IRenderer } from "./core.js";
1
+ import { IRenderer } from "./renderer.js";
2
2
  import { ParserParams, RenderParams } from "./interfaces.js";
3
3
  export declare class Renderer extends IRenderer {
4
+ readonly impl = "browser";
4
5
  protected readonly dirpath: string;
5
6
  parseHTML(content: string, params?: ParserParams): Document | DocumentFragment;
6
7
  serializeHTML(root: Node | DocumentFragment): string;
package/dist/browser.js CHANGED
@@ -1,7 +1,8 @@
1
- import { IRenderer } from "./core.js";
1
+ import { IRenderer } from "./renderer.js";
2
2
  import { dirname } from "./dome.js";
3
3
  export class Renderer extends IRenderer {
4
- dirpath = dirname(self.location.href);
4
+ impl = "browser";
5
+ dirpath = dirname(globalThis.location?.href ?? "http://localhost/");
5
6
  parseHTML(content, params = { rootDocument: false }) {
6
7
  if (params.rootDocument) {
7
8
  return new DOMParser().parseFromString(content, "text/html");
@@ -461,7 +461,7 @@ function posneg(props) {
461
461
  // Positive percent units.
462
462
  ...PERCENTS.map((v) => [`${klass}-${v}\\%`, `${prop}: ${v}%`]),
463
463
  // Negative percent units.
464
- ...PERCENTS.map((v) => [`-${klass}-${v}\\%`, ` ${prop}: -${v}%`]),
464
+ ...PERCENTS.map((v) => [`-${klass}-${v}\\%`, `${prop}: -${v}%`]),
465
465
  ])
466
466
  .flatMap(([klass, rule]) => [
467
467
  `.${klass} { ${rule} }`,
@@ -470,29 +470,35 @@ function posneg(props) {
470
470
  ]);
471
471
  }
472
472
  function autoxy(props) {
473
- return Object.entries(props).flatMap(([prop, klass]) => [
473
+ return Object.entries(props)
474
+ .flatMap(([prop, klass]) => [
474
475
  // Auto.
475
- `.${klass}-auto { ${prop}: auto; }`,
476
+ [`${klass}-auto`, `${prop}: auto`],
476
477
  // Auto x-axis.
477
- `.${klass}x-auto { ${prop}-left: auto; ${prop}-right: auto; }`,
478
+ [`${klass}x-auto`, `${prop}-left: auto; ${prop}-right: auto;`],
478
479
  // Auto y-axis.
479
- `.${klass}y-auto { ${prop}-top: auto; ${prop}-bottom: auto; }`,
480
+ [`${klass}y-auto`, `${prop}-top: auto; ${prop}-bottom: auto;`],
480
481
  // Zero x-axis.
481
- `.${klass}x-0 { ${prop}-left: 0; ${prop}-right: 0; }`,
482
+ [`${klass}x-0`, `${prop}-left: 0; ${prop}-right: 0;`],
482
483
  // Zero y-axis.
483
- `.${klass}y-0 { ${prop}-top: 0; ${prop}-bottom: 0; }`,
484
+ [`${klass}y-0`, `${prop}-top: 0; ${prop}-bottom: 0;`],
484
485
  // Positive REM units x-axis.
485
- ...UNITS_ALL.map((v) => [v, v * REM_UNIT]).map(([k, v]) => `.${klass}x-${k} { ${prop}-left: ${v}rem; ${prop}-right: ${v}rem; }`),
486
+ ...UNITS_ALL.map((v) => [v, v * REM_UNIT]).map(([k, v]) => [`${klass}x-${k}`, `${prop}-left: ${v}rem; ${prop}-right: ${v}rem;`]),
486
487
  // Positive REM units y-axis.
487
- ...UNITS_ALL.map((v) => [v, v * REM_UNIT]).map(([k, v]) => `.${klass}y-${k} { ${prop}-top: ${v}rem; ${prop}-bottom: ${v}rem; }`),
488
+ ...UNITS_ALL.map((v) => [v, v * REM_UNIT]).map(([k, v]) => [`${klass}y-${k}`, `${prop}-top: ${v}rem; ${prop}-bottom: ${v}rem;`]),
488
489
  // Positive PX units x-axis.
489
- ...UNITS_ALL.map((v) => `.${klass}x-${v}px { ${prop}-left: ${v}px; ${prop}-right: ${v}px; }`),
490
+ ...UNITS_ALL.map((v) => [`${klass}x-${v}px`, `${prop}-left: ${v}px; ${prop}-right: ${v}px;`]),
490
491
  // Positive PX units y-axis.
491
- ...UNITS_ALL.map((v) => `.${klass}y-${v}px { ${prop}-top: ${v}px; ${prop}-bottom: ${v}px; }`),
492
+ ...UNITS_ALL.map((v) => [`${klass}y-${v}px`, `${prop}-top: ${v}px; ${prop}-bottom: ${v}px;`]),
492
493
  // Positive percent units x-axis.
493
- ...PERCENTS.map((v) => `.${klass}x-${v}\\% { ${prop}-left: ${v}%; ${prop}-right: ${v}%; }`),
494
+ ...PERCENTS.map((v) => [`${klass}x-${v}\\%`, `${prop}-left: ${v}%; ${prop}-right: ${v}%;`]),
494
495
  // Positive percent units y-axis.
495
- ...PERCENTS.map((v) => `.${klass}y-${v}\\% { ${prop}-top: ${v}%; ${prop}-bottom: ${v}%; }`),
496
+ ...PERCENTS.map((v) => [`${klass}y-${v}\\%`, `${prop}-top: ${v}%; ${prop}-bottom: ${v}%;`]),
497
+ ])
498
+ .flatMap(([klass, rule]) => [
499
+ `.${klass} { ${rule} }`,
500
+ `${wrapPseudoStates(klass).join(",")} { ${rule} }`,
501
+ ...wrapMediaQueries(klass, rule),
496
502
  ]);
497
503
  }
498
504
  function tblr(props) {
@@ -573,32 +579,36 @@ function border() {
573
579
  function between() {
574
580
  return [
575
581
  // Zero for x margin.
576
- `.space-x-0 > * { margin-left: 0 }`,
582
+ [`space-x-0 > *`, `margin-left: 0`],
577
583
  // Zero for y margin.
578
- `.space-y-0 > * { margin-top: 0 }`,
584
+ [`space-y-0 > *`, `margin-top: 0`],
579
585
  // Positive REM units for x margin.
580
- ...UNITS_ALL.map((v) => `.space-x-${v} > :not(:first-child) { margin-left: ${v * REM_UNIT}rem }`),
586
+ ...UNITS_ALL.map((v) => [`space-x-${v} > :not(:first-child)`, `margin-left: ${v * REM_UNIT}rem`]),
581
587
  // Positive REM units for y margin.
582
- ...UNITS_ALL.map((v) => `.space-y-${v} > :not(:first-child) { margin-top: ${v * REM_UNIT}rem }`),
588
+ ...UNITS_ALL.map((v) => [`space-y-${v} > :not(:first-child)`, `margin-top: ${v * REM_UNIT}rem`]),
583
589
  // Positive PX units for x margin.
584
- ...UNITS_ALL.map((v) => `.space-x-${v}px > :not(:first-child) { margin-left: ${v}px }`),
590
+ ...UNITS_ALL.map((v) => [`space-x-${v}px > :not(:first-child)`, `margin-left: ${v}px`]),
585
591
  // Positive PX units for y margin.
586
- ...UNITS_ALL.map((v) => `.space-y-${v}px > :not(:first-child) { margin-top: ${v}px }`),
592
+ ...UNITS_ALL.map((v) => [`space-y-${v}px > :not(:first-child)`, `margin-top: ${v}px`]),
587
593
  // Zero for gap.
588
- `.gap-0 { gap: 0 }`,
594
+ [`gap-0`, `gap: 0`],
589
595
  // Positive REM units for gap.
590
- ...UNITS_ALL.map((v) => `.gap-${v} { gap: ${v * REM_UNIT}rem }`),
596
+ ...UNITS_ALL.map((v) => [`gap-${v}`, `gap: ${v * REM_UNIT}rem`]),
591
597
  // Positive PX units for gap.
592
- ...UNITS_ALL.map((v) => `.gap-${v}px { gap: ${v}px }`),
598
+ ...UNITS_ALL.map((v) => [`gap-${v}px`, `gap: ${v}px`]),
593
599
  // Positive REM units for col gap.
594
- ...UNITS_ALL.map((v) => `.gap-x-${v} { column-gap: ${v * REM_UNIT}rem }`),
600
+ ...UNITS_ALL.map((v) => [`gap-x-${v}`, `column-gap: ${v * REM_UNIT}rem`]),
595
601
  // Positive REM units for row gap.
596
- ...UNITS_ALL.map((v) => `.gap-y-${v} { row-gap: ${v * REM_UNIT}rem }`),
602
+ ...UNITS_ALL.map((v) => [`gap-y-${v}`, `row-gap: ${v * REM_UNIT}rem`]),
597
603
  // Positive PX units for col gap.
598
- ...UNITS_ALL.map((v) => `.gap-x-${v}px { column-gap: ${v}px }`),
604
+ ...UNITS_ALL.map((v) => [`gap-x-${v}px`, `column-gap: ${v}px`]),
599
605
  // Positive PX units for row gap.
600
- ...UNITS_ALL.map((v) => `.gap-y-${v}px { row-gap: ${v}px }`),
601
- ];
606
+ ...UNITS_ALL.map((v) => [`gap-y-${v}px`, `row-gap: ${v}px`]),
607
+ ].flatMap(([klass, rule]) => [
608
+ `.${klass} { ${rule} }`,
609
+ `${wrapPseudoStates(klass).join(",")} { ${rule} }`,
610
+ ...wrapMediaQueries(klass, rule),
611
+ ]);
602
612
  }
603
613
  function custom() {
604
614
  return Object.entries(PROPS_CUSTOM).flatMap(([klass, props]) => Object.entries(props).flatMap(([propkey, propval]) => [
package/dist/dome.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ type ElementWithAttribs = Element & {
2
+ dataset?: DOMStringMap;
3
+ attribs?: {
4
+ [key: string]: string;
5
+ };
6
+ };
1
7
  /**
2
8
  * Traverses the DOM tree starting from the given root node and yields each child node.
3
9
  * Nodes in the `skip` set will be skipped during traversal.
@@ -15,10 +21,14 @@ export declare function hasFunction(obj: any, func: string): boolean;
15
21
  * @returns camel-cased attribute name
16
22
  */
17
23
  export declare function attributeNameToCamelCase(name: string): string;
18
- export declare function getAttribute(elem: Element | any, name: string): string | null;
19
- export declare function setAttribute(elem: Element | any, name: string, value: string): void;
20
- export declare function removeAttribute(elem: Element | any, name: string): void;
21
- export declare function cloneAttribute(elemFrom: Element | any, elemDest: Element | any, name: string): void;
24
+ export declare function getAttribute(elem: ElementWithAttribs, name: string): string | null;
25
+ export declare function getAttributeOrDataset(elem: ElementWithAttribs, name: string, attributePrefix?: string): string | null;
26
+ export declare function setAttribute(elem: ElementWithAttribs, name: string, value: string): void;
27
+ export declare function safeSetAttribute(elem: ElementWithAttribs, name: string, value: string): void;
28
+ export declare function setProperty(elem: ElementWithAttribs, name: string, value: any): void;
29
+ export declare function removeAttribute(elem: ElementWithAttribs, name: string): void;
30
+ export declare function removeAttributeOrDataset(elem: ElementWithAttribs, name: string, prefix?: string): void;
31
+ export declare function cloneAttribute(elemFrom: ElementWithAttribs, elemDest: ElementWithAttribs, name: string): void;
22
32
  export declare function firstElementChild(elem: Element): Element | null;
23
33
  export declare function replaceWith(original: ChildNode, ...replacement: Node[]): void;
24
34
  export declare function replaceChildren(parent: ParentNode, ...nodes: Node[]): void;
@@ -40,3 +50,4 @@ export declare function dirname(fpath: string): string;
40
50
  * @returns A boolean indicating whether the file path is relative or not.
41
51
  */
42
52
  export declare function isRelativePath(fpath: string): boolean;
53
+ export {};
package/dist/dome.js CHANGED
@@ -1,10 +1,6 @@
1
1
  import { safeAttrPrefix } from "safevalues";
2
2
  import { safeElement } from "safevalues/dom";
3
- const SAFE_ATTRS = [
4
- safeAttrPrefix `:`,
5
- safeAttrPrefix `style`,
6
- safeAttrPrefix `class`,
7
- ];
3
+ const SAFE_ATTRS = [safeAttrPrefix `:`, safeAttrPrefix `style`, safeAttrPrefix `class`];
8
4
  /**
9
5
  * Traverses the DOM tree starting from the given root node and yields each child node.
10
6
  * Nodes in the `skip` set will be skipped during traversal.
@@ -49,27 +45,62 @@ export function getAttribute(elem, name) {
49
45
  if (hasProperty(elem, "attribs"))
50
46
  return elem.attribs?.[name] ?? null;
51
47
  else
52
- return elem.getAttribute?.(name);
48
+ return elem.getAttribute?.(name) ?? null;
49
+ }
50
+ export function getAttributeOrDataset(elem, name, attributePrefix = "") {
51
+ return (getAttribute(elem, attributePrefix + name) ||
52
+ (elem.dataset?.[attributeNameToCamelCase(name)] ?? null));
53
53
  }
54
54
  export function setAttribute(elem, name, value) {
55
+ if (hasProperty(elem, "attribs"))
56
+ elem.attribs[name] = value;
57
+ else
58
+ elem.setAttribute?.(name, value);
59
+ }
60
+ export function safeSetAttribute(elem, name, value) {
55
61
  if (hasProperty(elem, "attribs"))
56
62
  elem.attribs[name] = value;
57
63
  else
58
64
  safeElement.setPrefixedAttribute(SAFE_ATTRS, elem, name, value);
59
65
  }
66
+ export function setProperty(elem, name, value) {
67
+ switch (name) {
68
+ // Directly set some safe, known properties.
69
+ case "disabled":
70
+ elem.disabled = value;
71
+ return;
72
+ case "selected":
73
+ elem.selected = value;
74
+ return;
75
+ case "checked":
76
+ elem.checked = value;
77
+ return;
78
+ // Fall back to setting the property directly (unsafe).
79
+ default:
80
+ elem[name] = value;
81
+ }
82
+ }
60
83
  export function removeAttribute(elem, name) {
61
84
  if (hasProperty(elem, "attribs"))
62
85
  delete elem.attribs[name];
63
86
  else
64
87
  elem.removeAttribute?.(name);
65
88
  }
89
+ export function removeAttributeOrDataset(elem, name, prefix = "") {
90
+ removeAttribute(elem, `${prefix}${name}`);
91
+ removeAttribute(elem, `data-${name}`);
92
+ }
66
93
  export function cloneAttribute(elemFrom, elemDest, name) {
67
94
  if (hasProperty(elemFrom, "attribs") && hasProperty(elemDest, "attribs")) {
68
95
  elemDest.attribs[name] = elemFrom.attribs[name];
69
96
  }
97
+ else if (name.startsWith("data-")) {
98
+ const datasetKey = attributeNameToCamelCase(name.slice(5));
99
+ elemDest.dataset[datasetKey] = elemFrom.dataset?.[datasetKey];
100
+ }
70
101
  else {
71
102
  const attr = elemFrom?.getAttribute?.(name);
72
- setAttribute(elemDest, name, attr || "");
103
+ safeSetAttribute(elemDest, name, attr || "");
73
104
  }
74
105
  }
75
106
  export function firstElementChild(elem) {
@@ -145,6 +176,11 @@ export function ellipsize(str, maxLength = 0) {
145
176
  return str.slice(0, maxLength - 1) + "…";
146
177
  }
147
178
  export function nodeToString(node, maxLength = 0) {
179
+ if (globalThis.DocumentFragment && node instanceof DocumentFragment) {
180
+ return Array.from(node.childNodes)
181
+ .map((node) => nodeToString(node, maxLength))
182
+ .join("");
183
+ }
148
184
  return ellipsize(node.outerHTML || node.nodeValue || String(node), maxLength);
149
185
  }
150
186
  /**
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ParserParams, RenderParams } from "./interfaces.js";
2
- import { IRenderer } from "./core.js";
2
+ import { IRenderer } from "./renderer.js";
3
3
  export declare class Renderer extends IRenderer {
4
+ readonly impl = "jsdom";
4
5
  parseHTML(content: string, params?: ParserParams): Document | DocumentFragment;
5
6
  serializeHTML(root: Node | DocumentFragment | Document): string;
6
7
  createElement(tag: string, owner?: Document | null): Element;
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as fs from "fs/promises";
2
2
  import { JSDOM } from "jsdom";
3
- import { IRenderer } from "./core.js";
3
+ import { IRenderer } from "./renderer.js";
4
4
  export class Renderer extends IRenderer {
5
+ impl = "jsdom";
5
6
  parseHTML(content, params = { rootDocument: false }) {
6
7
  if (params.rootDocument) {
7
8
  return new JSDOM(content).window.document;
@@ -1,4 +1,4 @@
1
- import { IRenderer } from "./core.js";
1
+ import { IRenderer } from "./renderer.js";
2
2
  export interface ParserParams {
3
3
  /** Whether the file parsed is a root document, or a document fragment. */
4
4
  rootDocument?: boolean;