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.
- package/README.md +112 -0
- package/index.d.ts +10 -0
- package/index.js +98 -0
- 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
|
+

|
|
6
|
+

|
|
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
|
+
}
|