mikel-jsx 0.40.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.
Files changed (4) hide show
  1. package/README.md +112 -0
  2. package/index.d.ts +10 -0
  3. package/index.js +98 -0
  4. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # mikel-jsx
2
+
3
+ > **⚠️ Experimental.** This package is a work in progress — the tag syntax, attribute rules, and public API can change at any time without notice. Not recommended for production use yet.
4
+
5
+ ![npm version](https://badgen.net/npm/v/mikel-jsx?labelColor=1d2734&color=21bf81)
6
+ ![license](https://badgen.net/github/license/jmjuanes/mikel?labelColor=1d2734&color=21bf81)
7
+
8
+ A plugin for [mikel](https://github.com/jmjuanes/mikel) that adds a JSX-style `<m-name>` tag for calling mikel directives (helpers and partials) from HTML-ish templates, as an alternative to the usual mustache syntax (`{{# }}`).
9
+
10
+ ```html
11
+ <!-- before -->
12
+ {{#header title="Hello" /}}
13
+ {{#if isAdmin}}Admin!{{/if}}
14
+
15
+ <!-- now, also available -->
16
+ <m-header title="Hello" />
17
+ <m-if isAdmin>Admin!</m-if>
18
+ ```
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ # install using NPM
24
+ $ npm install mikel-jsx
25
+
26
+ # install using YARN
27
+ $ yarn add mikel-jsx
28
+ ```
29
+
30
+ ```js
31
+ import mikel from "mikel";
32
+ import mikelJsx from "mikel-jsx";
33
+
34
+ const mk = mikel.create({ ... });
35
+
36
+ mk.use(mikelJsx());
37
+ ```
38
+
39
+ ## Tag syntax
40
+
41
+ Only tags whose name starts exactly with `m-` are recognized; any other tag (`<div>`, `<span>`, your own custom elements) is left completely untouched.
42
+
43
+ ```html
44
+ <m-header title="Hello" />
45
+ <!-- {{#header title="Hello" /}} -->
46
+
47
+ <m-card title="Hello">
48
+ This content is available inside the partial as {{@content}}
49
+ </m-card>
50
+ <!-- {{#card title="Hello"}}...{{/card}} -->
51
+
52
+ <m-if isAdmin>You're an admin</m-if>
53
+ <!-- {{#if isAdmin}}...{{/if}} -->
54
+
55
+ <m-each users limit="3">{{this.name}}</m-each>
56
+ <!-- {{#each users limit="3"}}...{{/each}} -->
57
+ ```
58
+
59
+ A tag with no attributes and a self-closing slash, `<m-slot />`, becomes `{{#slot /}}` — same rule, nothing special about it.
60
+
61
+ ## Attributes
62
+
63
+ Each attribute is translated into a mikel argument (positional or keyword). There are just two things a value can be, and they mean the same thing whether the attribute is positional or keyword:
64
+
65
+ - **quoted (`"..."`)**: always a plain string literal. `"5"` and `"true"` stay strings.
66
+ - **braced (`{...}`)**: a raw mikel value: a variable path, a number, a boolean, a subexpression, or a spread. Whatever you put inside the braces is handed straight to mikel's own parser.
67
+
68
+ | Written as | Translates to... | Resulting type |
69
+ |--------------|-------------------|----------------------------------------------------------|
70
+ | `name` | `name` | positional, variable/path (dots allowed: `user.name`) |
71
+ | `key="text"` | `key="text"` | keyword, string literal, always |
72
+ | `key={expr}` | `key=expr` | keyword, raw value (variable, string, number, boolean, subexpression...) |
73
+ | `{expr}` | `expr` | positional, raw value (for anything that isn't a plain identifier, e.g. `{(eq a b)}`) |
74
+ | `{...expr}` | `...expr` | spread |
75
+
76
+ Examples:
77
+
78
+ ```html
79
+ <m-each users limit="3">...</m-each>
80
+ <!-- {{#each users limit="3"}}...{{/each}} -->
81
+ <!-- careful: this passes the STRING "3", not the number 3 -->
82
+
83
+ <m-each users limit={3} skip={pageOffset}>...</m-each>
84
+ <!-- {{#each users limit=3 skip=pageOffset}}...{{/each}} -->
85
+
86
+ <m-user {...person} />
87
+ <!-- {{#user ...person /}} -->
88
+ ```
89
+
90
+ ## Nesting
91
+
92
+ Tags can be nested arbitrarily deep — the transform always resolves the innermost pair first:
93
+
94
+ ```html
95
+ <m-card title="Outer">
96
+ <m-if isAdmin>
97
+ <m-each users>{{this.name}} </m-each>
98
+ </m-if>
99
+ </m-card>
100
+ ```
101
+
102
+ ## Known limitations
103
+
104
+ - An unescaped `>` inside a literal attribute breaks tag recognition (no HTML-specific escaping is performed).
105
+ - Quotes must be straight (`"`), not curly/typographic (`“` `”`) — check that your editor isn't auto-correcting them.
106
+ - Only double quotes (`"`) are recognized for attribute values, by design — single quotes (`'...'`) are intentionally not supported.
107
+ - Literal content inside an example `<pre>`/`<code>` block would be transformed just like any other content — there's no escape hatch.
108
+ - The `m-` prefix is effectively reserved: if you already use real custom elements with that prefix, they will collide.
109
+
110
+ ## License
111
+
112
+ Licensed under the [MIT License](../../LICENSE).
package/index.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { MikelTransform } from "mikel";
2
+
3
+ declare function mikelJsxPlugin(): {
4
+ transform: MikelTransform,
5
+ };
6
+
7
+ export default mikelJsxPlugin;
8
+
9
+ export declare function transform(content: string): string;
10
+ export declare function parseAttributes(raw?: string): string;
package/index.js ADDED
@@ -0,0 +1,98 @@
1
+ //
2
+ // Adds a single JSX-style <m-name> tag for calling mikel directives
3
+ // (helpers and partials — both share the same `#` mechanism now, so
4
+ // there's nothing to disambiguate). One prefix, one output shape:
5
+ //
6
+ // <m-header title="Hello" /> --> {{#header title="Hello" /}}
7
+ // <m-if isAdmin>...</m-if> --> {{#if isAdmin}}...{{/if}}
8
+ // <m-each users skip={1} />...</m-each> --> {{#each users skip=1}}...{{/each}}
9
+ //
10
+ // Other tags (<div>, <span>, your own custom elements) are left untouched.
11
+ //
12
+ // Attribute rules:
13
+ // name --> positional, raw value (bare attribute, dotted paths ok)
14
+ // key="literal" --> keyword, string literal, always — never auto-typed
15
+ // key={expr} --> keyword, raw value (variable, string, number, boolean, subexpression...)
16
+ // key="{expr}" --> same as key={expr}, quoted so HTML linters don't flag it (undocumented, but tested)
17
+ // {expr} --> positional, raw value (for values that aren't identifiers)
18
+ // {...expr} --> spread
19
+
20
+ // @description converts a raw HTML-like attribute string into a mikel argument string.
21
+ // Supports, in this order of precedence (order matters for correct matching):
22
+ // {...expr} --> spread e.g. {...user}
23
+ // key="{expr}" --> keyword, raw value, quoted escape hatch e.g. limit="{pageSize}"
24
+ // key="text" --> keyword, string literal, always e.g. title="Hello"
25
+ // key={expr} --> keyword, raw value e.g. limit={pageSize}, flag={true}
26
+ // {expr} --> positional, raw value e.g. {(eq a b)}
27
+ // name --> positional, raw value (bare attr) e.g. isAdmin, users
28
+ const ATTR_RE = /\{\s*\.\.\.\s*([^}]+?)\s*\}|([a-zA-Z_][\w-]*)\s*=\s*"\{\s*([^}]+?)\s*\}"|([a-zA-Z_][\w-]*)\s*=\s*"([^"]*)"|([a-zA-Z_][\w-]*)\s*=\s*\{\s*([^}]+?)\s*\}|\{\s*([^}]+?)\s*\}|([a-zA-Z_][\w.-]*)/g;
29
+
30
+ export const parseAttributes = (raw = "") => {
31
+ const parts = [];
32
+ let m;
33
+ ATTR_RE.lastIndex = 0;
34
+ while ((m = ATTR_RE.exec(raw))) {
35
+ const [, spread, quotedBracedKey, quotedBracedExpr, literalKey, literalValue, bracedKey, bracedExpr, bareExpr, bareName] = m;
36
+ if (spread !== undefined) {
37
+ parts.push(`...${spread.trim()}`);
38
+ } else if (quotedBracedKey !== undefined) {
39
+ parts.push(`${quotedBracedKey}=${quotedBracedExpr.trim()}`);
40
+ } else if (literalKey !== undefined) {
41
+ parts.push(`${literalKey}="${literalValue}"`);
42
+ } else if (bracedKey !== undefined) {
43
+ parts.push(`${bracedKey}=${bracedExpr.trim()}`);
44
+ } else if (bareExpr !== undefined) {
45
+ parts.push(bareExpr.trim());
46
+ } else if (bareName !== undefined) {
47
+ parts.push(bareName);
48
+ }
49
+ }
50
+ return parts.join(" ");
51
+ };
52
+
53
+ // matches a self-closing tag: <m-name attrs />
54
+ const SELF_CLOSING_RE = /<m-([a-zA-Z_][\w-]*)((?:\s+[^<>]*?)?)\s*\/>/;
55
+
56
+ // matches an innermost paired tag: <m-name attrs>...</m-name>. The negative
57
+ // lookahead in the content group stops the match before any nested custom
58
+ // tag, so the innermost pair is always resolved first.
59
+ const PAIRED_RE = /<m-([a-zA-Z_][\w-]*)((?:\s+[^<>]*?)?)\s*>((?:(?!<m-)[\s\S])*?)<\/m-\1\s*>/;
60
+
61
+ // @description replaces one match at a time, re-searching after each
62
+ // replacement (needed since nested tags only become matchable once their
63
+ // inner tags have already been resolved)
64
+ const replaceAll = (str, re, replacer) => {
65
+ let match;
66
+ while ((match = re.exec(str))) {
67
+ str = str.slice(0, match.index) + replacer(match) + str.slice(match.index + match[0].length);
68
+ }
69
+ return str;
70
+ };
71
+
72
+ // @description the actual template transform: string in, string out.
73
+ // No lookup, no ambiguity: every <m-name> becomes {{#name ...}}, since
74
+ // helpers and partials are now the same underlying mechanism in mikel.
75
+ export const transform = content => {
76
+ // 1. self-closing tags first: no nesting to worry about
77
+ content = replaceAll(content, SELF_CLOSING_RE, ([, name, attrs]) => {
78
+ const args = parseAttributes(attrs);
79
+ const suffix = args ? ` ${args}` : "";
80
+ return `{{#${name}${suffix} /}}`;
81
+ });
82
+ // 2. paired (block) tags, innermost first, repeated until none are left
83
+ content = replaceAll(content, PAIRED_RE, ([, name, attrs, inner]) => {
84
+ const args = parseAttributes(attrs);
85
+ const suffix = args ? ` ${args}` : "";
86
+ return `{{#${name}${suffix}}}${inner}{{/${name}}}`;
87
+ });
88
+ return content;
89
+ };
90
+
91
+ // @description mikel plugin: a factory returning a plain options object for
92
+ // mk.use(), following the same convention as mikel.SetStatePlugin — call it
93
+ // to get the object, then pass that to use(): mk.use(mikelJsx())
94
+ export default () => {
95
+ return {
96
+ transform: transform,
97
+ };
98
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "mikel-jsx",
3
+ "description": "JSX/XML style tags for mikel templating.",
4
+ "version": "0.40.0",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Josemi Juanes",
9
+ "email": "hello@josemi.xyz"
10
+ },
11
+ "repository": {
12
+ "url": "https://github.com/jmjuanes/mikel",
13
+ "directory": "packages/mikel-jsx"
14
+ },
15
+ "bugs": "https://github.com/jmjuanes/mikel/issues",
16
+ "types": "index.d.ts",
17
+ "scripts": {
18
+ "test": "node test.js"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "import": "./index.js",
23
+ "types": "./index.d.ts"
24
+ },
25
+ "./index.js": {
26
+ "import": "./index.js",
27
+ "types": "./index.d.ts"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "files": [
32
+ "README.md",
33
+ "index.js",
34
+ "index.d.ts"
35
+ ],
36
+ "keywords": [
37
+ "mikel",
38
+ "jsx",
39
+ "xml",
40
+ "templating"
41
+ ]
42
+ }