@todoist/react-interpolate 0.0.0-stage → 2.2.5
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/LICENSE +21 -0
- package/README.md +221 -2
- package/dist/react-interpolate.cjs +444 -0
- package/dist/react-interpolate.d.ts +18 -0
- package/dist/react-interpolate.min.cjs +1 -0
- package/dist/react-interpolate.min.mjs +1 -0
- package/dist/react-interpolate.mjs +426 -0
- package/package.json +81 -5
- package/src/constants.js +11 -0
- package/src/index.js +5 -0
- package/src/interpolate.js +93 -0
- package/src/lexer.js +62 -0
- package/src/node.js +65 -0
- package/src/parser.js +166 -0
- package/src/react-interpolate.d.ts +18 -0
- package/src/syntax.js +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Doist
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,222 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-interpolate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](http://img.badgesize.io/https://cdn.jsdelivr.net/gh/todoist/react-interpolate/dist/react-interpolate.min.cjs?compression=gzip) [](https://github.com/todoist/react-interpolate/actions)
|
|
4
|
+
|
|
5
|
+
A string interpolation component that formats and interpolates a template string in a safe way.
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import Interpolate from '@todoist/react-interpolate'
|
|
9
|
+
|
|
10
|
+
function Greeting() {
|
|
11
|
+
return (
|
|
12
|
+
<Interpolate
|
|
13
|
+
string="<h1>Hello {name}. Here is <a>your order info</a></h1>"
|
|
14
|
+
mapping={{
|
|
15
|
+
name: 'William',
|
|
16
|
+
a: <a href="https://orderinfo.com" />,
|
|
17
|
+
}}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Would render the following HTML
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<h1>Hello William. Here is <a href="https://orderinfo.com">your order info</a></h1>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Component API
|
|
30
|
+
|
|
31
|
+
`<Interpolate>` component accepts the following props
|
|
32
|
+
|
|
33
|
+
#### `string`
|
|
34
|
+
|
|
35
|
+
The template string to be interpolated. Required.
|
|
36
|
+
|
|
37
|
+
Please see the [Interpolation syntax](./#interpolation-syntax) section below for more detail.
|
|
38
|
+
|
|
39
|
+
#### `mapping`
|
|
40
|
+
|
|
41
|
+
An object that defines the values to be injected for placeholder and tags defined in the template string. Optional.
|
|
42
|
+
|
|
43
|
+
- For placeholder or self-closing tag, the mapping value could be any valid element value
|
|
44
|
+
- For open & close tag, the mapping value could be either renderer function or an element.
|
|
45
|
+
|
|
46
|
+
```jsx
|
|
47
|
+
<Interpolate
|
|
48
|
+
string="Hello {name}. Here is <orderLink>your order info</orderLink><hr/>. \
|
|
49
|
+
Please contact <supportLink>support</supportLink> for help"
|
|
50
|
+
mapping={{
|
|
51
|
+
// you can map placholder and self-closing tag to any valid element value
|
|
52
|
+
name: 'William',
|
|
53
|
+
hr: <hr className="break" />,
|
|
54
|
+
|
|
55
|
+
// you can map open & close tag to a rendering function
|
|
56
|
+
orderLink: (text) => <a href="https://orderinfo.com">{text}</a>,
|
|
57
|
+
|
|
58
|
+
// or you can map open & close tag to an element
|
|
59
|
+
supportLink: <a href="https://orderinfo.com" />,
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### `graceful`
|
|
65
|
+
|
|
66
|
+
A boolean flag indicates how string syntax error or mapping error should be handled. When true, the raw string value from the prop `string` would be rendered as a fallback in the error scenario. When false, error would be thrown instead.
|
|
67
|
+
|
|
68
|
+
Optional. `true` by default.
|
|
69
|
+
|
|
70
|
+
```jsx
|
|
71
|
+
// would render "an invalid string with unclose tag <h1>"
|
|
72
|
+
<Interpolate graceful string="an invalid string with unclose tag <h1>" />
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### `syntax`
|
|
76
|
+
|
|
77
|
+
Optional. `syntax` props allow use of react-Interpolate with different string formatting syntax. Please see the ["Custom syntax support"](#custom-syntax-support) section for more detail.
|
|
78
|
+
|
|
79
|
+
## Interpolation syntax
|
|
80
|
+
|
|
81
|
+
Here is interpolation syntax you can use in your `string`.
|
|
82
|
+
|
|
83
|
+
#### Placeholder
|
|
84
|
+
|
|
85
|
+
```jsx
|
|
86
|
+
'hello {user_name}'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Placeholder name should be alphanumeric (`[A-Za-z0-9_]`). Placeholders could be mapped to any valid element value.
|
|
90
|
+
|
|
91
|
+
#### Open & close tags
|
|
92
|
+
|
|
93
|
+
```jsx
|
|
94
|
+
'Here is <a>your order info</a>'
|
|
95
|
+
|
|
96
|
+
// tag name could be any alphanumeric string
|
|
97
|
+
'Here is <link>your order info</link>'
|
|
98
|
+
|
|
99
|
+
// you can nest tag and placeholder
|
|
100
|
+
'Here is <a><b>you order info {name}</b></a>'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Tag name should be alphanumeric (`[A-Za-z0-9_]`).
|
|
104
|
+
|
|
105
|
+
Open & close tag could be mapped to an element value.
|
|
106
|
+
|
|
107
|
+
```jsx
|
|
108
|
+
<Interpolate
|
|
109
|
+
string="Here is <a>your order info</a>"
|
|
110
|
+
mapping={{
|
|
111
|
+
a: <a href="https://orderinfo.com" />
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
// Invalid; the mapping value element should not contain children
|
|
117
|
+
<Interpolate
|
|
118
|
+
string="Here is <a>your order info</a>"
|
|
119
|
+
mapping={{
|
|
120
|
+
a: (
|
|
121
|
+
<a href="https://orderinfo.com">
|
|
122
|
+
<b />
|
|
123
|
+
<br />
|
|
124
|
+
</a>
|
|
125
|
+
)
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Open & close tag could be mapped to a rendering function, which would take a single argument that contains the enclosing text.
|
|
131
|
+
|
|
132
|
+
```jsx
|
|
133
|
+
<Interpolate
|
|
134
|
+
string="Here is <a>your order info</a>"
|
|
135
|
+
mapping={{
|
|
136
|
+
a: (text) => (
|
|
137
|
+
<a href="https://orderinfo.com">
|
|
138
|
+
<b>{text}</b>
|
|
139
|
+
<br />
|
|
140
|
+
</a>
|
|
141
|
+
),
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Unclosed tag or incorrect nesting of tag would result in syntax error.
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
// bad: no close tag
|
|
150
|
+
'Here is <a>your order info'
|
|
151
|
+
|
|
152
|
+
// bad: incorrect tag structure
|
|
153
|
+
'Here is <a><b>your order info</a></b>'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Self closing tag
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
'Hello.<br/>Here is your order'
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Tag name should be alphanumeric (`[A-Za-z0-9_]`). Self closing tags could be mapped to any valid element value.
|
|
163
|
+
|
|
164
|
+
## Auto tag element creation
|
|
165
|
+
|
|
166
|
+
When tags are used the string but there are no correponding mapped value, it would by default create the corresponding HTML element by default.
|
|
167
|
+
|
|
168
|
+
```jsx
|
|
169
|
+
// would render: <h1>Hellow</h1><br/>World
|
|
170
|
+
<Interpolate string="<h1>Hello</h1><br/>world" />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Custom syntax support
|
|
174
|
+
|
|
175
|
+
You may already be using a formatting syntax in your string that is different than the built-in syntax support from Interpolate. You can configure Interpolate so that it could recognize the formatting syntax that you use.
|
|
176
|
+
|
|
177
|
+
For instance, you may be using [i18next](https://www.i18next.com/) which has a slightly different placeholder syntax.
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
hello {{name}}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
You can define the formatting syntax of your string via `syntax` props.
|
|
184
|
+
|
|
185
|
+
```jsx
|
|
186
|
+
import Interpolate, { TOKEN_PLACEHOLDER } from "@todoist/react-interpolate"
|
|
187
|
+
|
|
188
|
+
const i18nNextSyntax = [
|
|
189
|
+
{
|
|
190
|
+
type: TOKEN_PLACEHOLDER,
|
|
191
|
+
regex: /{{\s*(\w+)\s*}}/g
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
// will output "hi steven"
|
|
196
|
+
<Interpolate
|
|
197
|
+
syntax={i18nNextSyntax}
|
|
198
|
+
string="hi {{name}}"
|
|
199
|
+
mapping={{
|
|
200
|
+
name: "steven"
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
react-interpolate comes with i18next syntax support, and you can enable it via
|
|
206
|
+
|
|
207
|
+
```jsx
|
|
208
|
+
import { SYNTAX_I18NEXT } from "@todoist/react-interpolate"
|
|
209
|
+
|
|
210
|
+
<Interpolate
|
|
211
|
+
syntax={SYNTAX_I18NEXT}
|
|
212
|
+
...
|
|
213
|
+
/>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
# Releasing
|
|
217
|
+
|
|
218
|
+
See [the release runbook](docs/releasing.md). Versions still follow semantic-release's Conventional Commits and the existing `v${version}` tag lineage: `fix:` selects a patch, `feat:` a minor and breaking changes a major. The first intended new-scope stable release is `@todoist/react-interpolate@2.2.5`, subject to actual history and collision checks.
|
|
219
|
+
|
|
220
|
+
Pushes to `main` and `next` calculate the normal stable or `next.N` version and prepare a retained artifact without publishing. Authorized publication is an explicit protected operation: npm and GitHub Packages receive the same tarball, including bootstrap and missing-mirror retries. After both registry copies match, the release record commits the generated version/changelog back to `main`, creates the normal version tag and semantic-release channel note, and completes the GitHub release. `next` keeps its prerelease channel and skips source commitback; it never promotes `latest`.
|
|
221
|
+
|
|
222
|
+
Keep the original successful run and artifact. Publication/record retries do not recalculate the version or rebuild the package, including after later workflow maintenance.
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var React = require('react');
|
|
6
|
+
var PropTypes = require('prop-types');
|
|
7
|
+
var _classCallCheck = require('@babel/runtime/helpers/classCallCheck');
|
|
8
|
+
var _createClass = require('@babel/runtime/helpers/createClass');
|
|
9
|
+
var _objectWithoutProperties = require('@babel/runtime/helpers/objectWithoutProperties');
|
|
10
|
+
|
|
11
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
12
|
+
|
|
13
|
+
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
|
|
14
|
+
var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
|
|
15
|
+
var _classCallCheck__default = /*#__PURE__*/_interopDefaultLegacy(_classCallCheck);
|
|
16
|
+
var _createClass__default = /*#__PURE__*/_interopDefaultLegacy(_createClass);
|
|
17
|
+
var _objectWithoutProperties__default = /*#__PURE__*/_interopDefaultLegacy(_objectWithoutProperties);
|
|
18
|
+
|
|
19
|
+
var TOKEN_PLACEHOLDER = 'TOKEN_PLACEHOLDER';
|
|
20
|
+
var TOKEN_OPEN_TAG = 'TOKEN_OPEN_TAG';
|
|
21
|
+
var TOKEN_CLOSE_TAG = 'TOKEN_CLOSE_TAG';
|
|
22
|
+
var TOKEN_SELF_TAG = 'TOKEN_SELF_TAG';
|
|
23
|
+
var TOKEN_TEXT = 'TOKEN_TEXT';
|
|
24
|
+
var NODE_FRAGMENT = 'NODE_FRAGMENT';
|
|
25
|
+
var NODE_TAG_ELEMENT = 'NODE_TAG_ELEMENT';
|
|
26
|
+
var NODE_VOID_ELEMENT = 'NODE_VOID_ELEMENT';
|
|
27
|
+
var NODE_PLACEHOLDER = 'NODE_PLACEHOLDER';
|
|
28
|
+
var NODE_TEXT = 'NODE_TEXT';
|
|
29
|
+
|
|
30
|
+
var _excluded = ["type", "children"];
|
|
31
|
+
var Node = /*#__PURE__*/function () {
|
|
32
|
+
function Node(_ref) {
|
|
33
|
+
var type = _ref.type,
|
|
34
|
+
children = _ref.children,
|
|
35
|
+
fields = _objectWithoutProperties__default["default"](_ref, _excluded);
|
|
36
|
+
_classCallCheck__default["default"](this, Node);
|
|
37
|
+
this.type = type;
|
|
38
|
+
this.children = children || [];
|
|
39
|
+
for (var key in fields) {
|
|
40
|
+
this[key] = fields[key];
|
|
41
|
+
}
|
|
42
|
+
if (fields.token) {
|
|
43
|
+
this.string = fields.token.string;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
_createClass__default["default"](Node, [{
|
|
47
|
+
key: "appendChild",
|
|
48
|
+
value: function appendChild(child) {
|
|
49
|
+
this.children.push(child);
|
|
50
|
+
}
|
|
51
|
+
}, {
|
|
52
|
+
key: "isLeaf",
|
|
53
|
+
get: function get() {
|
|
54
|
+
return this.children.length === 0;
|
|
55
|
+
}
|
|
56
|
+
}]);
|
|
57
|
+
return Node;
|
|
58
|
+
}();
|
|
59
|
+
Node.createTagNode = function (token, children) {
|
|
60
|
+
return new Node({
|
|
61
|
+
type: NODE_TAG_ELEMENT,
|
|
62
|
+
children: children,
|
|
63
|
+
name: token.name,
|
|
64
|
+
token: token
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
Node.createFragmentNode = function (children) {
|
|
68
|
+
return new Node({
|
|
69
|
+
type: NODE_FRAGMENT,
|
|
70
|
+
children: children
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
Node.createVoidNode = function (token) {
|
|
74
|
+
return new Node({
|
|
75
|
+
type: NODE_VOID_ELEMENT,
|
|
76
|
+
name: token.name,
|
|
77
|
+
token: token
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
Node.createTextNode = function (token) {
|
|
81
|
+
return new Node({
|
|
82
|
+
type: NODE_TEXT,
|
|
83
|
+
text: token.string,
|
|
84
|
+
token: token
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
Node.createPlaceholderNode = function (token) {
|
|
88
|
+
return new Node({
|
|
89
|
+
type: NODE_PLACEHOLDER,
|
|
90
|
+
name: token.name,
|
|
91
|
+
token: token
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
var SYNTAX_BUILT_IN = [{
|
|
96
|
+
type: TOKEN_PLACEHOLDER,
|
|
97
|
+
regex: /{\s*(\w+)\s*}/g
|
|
98
|
+
}, {
|
|
99
|
+
type: TOKEN_OPEN_TAG,
|
|
100
|
+
regex: /<(\w+)>/g
|
|
101
|
+
}, {
|
|
102
|
+
type: TOKEN_CLOSE_TAG,
|
|
103
|
+
regex: /<\/(\w+)>/g
|
|
104
|
+
}, {
|
|
105
|
+
type: TOKEN_SELF_TAG,
|
|
106
|
+
regex: /<(\w+)\s*\/>/g
|
|
107
|
+
}];
|
|
108
|
+
var SYNTAX_I18NEXT = [{
|
|
109
|
+
type: TOKEN_PLACEHOLDER,
|
|
110
|
+
regex: /{{\s*(\w+)\s*}}/g
|
|
111
|
+
}, {
|
|
112
|
+
type: TOKEN_OPEN_TAG,
|
|
113
|
+
regex: /<(\w+)>/g
|
|
114
|
+
}, {
|
|
115
|
+
type: TOKEN_CLOSE_TAG,
|
|
116
|
+
regex: /<\/(\w+)>/g
|
|
117
|
+
}, {
|
|
118
|
+
type: TOKEN_SELF_TAG,
|
|
119
|
+
regex: /<(\w+)\s*\/>/g
|
|
120
|
+
}];
|
|
121
|
+
|
|
122
|
+
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
|
|
123
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
124
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) { n[e] = r[e]; } return n; }
|
|
125
|
+
|
|
126
|
+
/*
|
|
127
|
+
* return a array of token object
|
|
128
|
+
*/
|
|
129
|
+
function lexer(string, syntax) {
|
|
130
|
+
var matches = [];
|
|
131
|
+
var _iterator = _createForOfIteratorHelper(syntax),
|
|
132
|
+
_step;
|
|
133
|
+
try {
|
|
134
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
135
|
+
var rule = _step.value;
|
|
136
|
+
var type = rule.type,
|
|
137
|
+
regex = rule.regex;
|
|
138
|
+
var res;
|
|
139
|
+
while ((res = regex.exec(string)) !== null) {
|
|
140
|
+
matches.push({
|
|
141
|
+
type: type,
|
|
142
|
+
string: res[0],
|
|
143
|
+
name: res[1],
|
|
144
|
+
start: res.index,
|
|
145
|
+
end: res.index + res[0].length
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
_iterator.e(err);
|
|
151
|
+
} finally {
|
|
152
|
+
_iterator.f();
|
|
153
|
+
}
|
|
154
|
+
matches.sort(function (a, b) {
|
|
155
|
+
return a.start - b.start;
|
|
156
|
+
});
|
|
157
|
+
var textTokens = [];
|
|
158
|
+
var start = 0;
|
|
159
|
+
matches.forEach(function (match) {
|
|
160
|
+
if (match.start === start) {
|
|
161
|
+
start = match.end;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
var end = match.start;
|
|
165
|
+
var text = string.substring(start, end);
|
|
166
|
+
textTokens.push({
|
|
167
|
+
type: TOKEN_TEXT,
|
|
168
|
+
string: text,
|
|
169
|
+
text: text,
|
|
170
|
+
start: start,
|
|
171
|
+
end: end
|
|
172
|
+
});
|
|
173
|
+
start = match.end;
|
|
174
|
+
});
|
|
175
|
+
var text = string.substring(start);
|
|
176
|
+
if (text !== '') {
|
|
177
|
+
textTokens.push({
|
|
178
|
+
type: TOKEN_TEXT,
|
|
179
|
+
string: text,
|
|
180
|
+
start: start,
|
|
181
|
+
end: string.length
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
var tokens = [].concat(textTokens, matches).sort(function (a, b) {
|
|
185
|
+
return a.start - b.start;
|
|
186
|
+
});
|
|
187
|
+
return tokens;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parser(string, syntax) {
|
|
191
|
+
if (!syntax) {
|
|
192
|
+
syntax = SYNTAX_BUILT_IN;
|
|
193
|
+
}
|
|
194
|
+
var tokens = lexer(string, syntax);
|
|
195
|
+
var p = new Parser(tokens);
|
|
196
|
+
return p.parse();
|
|
197
|
+
}
|
|
198
|
+
var SYNTAX_ERROR = 'Syntax error. Please check if each open tag is closed correctly';
|
|
199
|
+
|
|
200
|
+
// A special token representing end of the tream
|
|
201
|
+
var EPSILON = {
|
|
202
|
+
type: 'EPSILON'
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/*
|
|
206
|
+
* A recursive descent parser that construct a syntax tree represeting
|
|
207
|
+
* the DOM hierarchy structure.
|
|
208
|
+
*
|
|
209
|
+
* For instance, the following token
|
|
210
|
+
* <h1>hi <b>{name}</b></h1>
|
|
211
|
+
*
|
|
212
|
+
* Would result in the following tree
|
|
213
|
+
*
|
|
214
|
+
* <h1>
|
|
215
|
+
* ├─ "hi"
|
|
216
|
+
* └─ <b>
|
|
217
|
+
* ├─ {name}
|
|
218
|
+
*
|
|
219
|
+
* The parser is based on the following grammar:
|
|
220
|
+
*
|
|
221
|
+
* document ::= elementOrData
|
|
222
|
+
* elementOrData ::= element elementOrData
|
|
223
|
+
* elementOrData ::= data elementOrData
|
|
224
|
+
* element ::= <open_tag> elementOrData <close_tag>
|
|
225
|
+
* data ::= <selfTag>
|
|
226
|
+
* data ::= <text>
|
|
227
|
+
* data ::= <placeholder>
|
|
228
|
+
*
|
|
229
|
+
* ref: https://github.com/amghannam/Mini-XML-Recursive-Descent-Parser
|
|
230
|
+
*/
|
|
231
|
+
var Parser = /*#__PURE__*/function () {
|
|
232
|
+
function Parser(tokens) {
|
|
233
|
+
_classCallCheck__default["default"](this, Parser);
|
|
234
|
+
this.tokens = [].concat(tokens);
|
|
235
|
+
this.tags = [];
|
|
236
|
+
}
|
|
237
|
+
_createClass__default["default"](Parser, [{
|
|
238
|
+
key: "parse",
|
|
239
|
+
value: function parse() {
|
|
240
|
+
var tree = this.document();
|
|
241
|
+
if (!this.predict(EPSILON.type)) {
|
|
242
|
+
throw new Error(SYNTAX_ERROR);
|
|
243
|
+
}
|
|
244
|
+
return tree;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/*
|
|
248
|
+
* @return {Node}
|
|
249
|
+
*/
|
|
250
|
+
}, {
|
|
251
|
+
key: "document",
|
|
252
|
+
value: function document() {
|
|
253
|
+
var children = this.elementOrData();
|
|
254
|
+
return Node.createFragmentNode(children);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/*
|
|
258
|
+
* @return {Node}
|
|
259
|
+
*/
|
|
260
|
+
}, {
|
|
261
|
+
key: "element",
|
|
262
|
+
value: function element() {
|
|
263
|
+
var tagToken = this.openTag();
|
|
264
|
+
var children = this.elementOrData();
|
|
265
|
+
this.endTag();
|
|
266
|
+
return Node.createTagNode(tagToken, children);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/*
|
|
270
|
+
* @return {Token}
|
|
271
|
+
*/
|
|
272
|
+
}, {
|
|
273
|
+
key: "openTag",
|
|
274
|
+
value: function openTag() {
|
|
275
|
+
if (!this.predict(TOKEN_OPEN_TAG)) {
|
|
276
|
+
throw new Error(SYNTAX_ERROR);
|
|
277
|
+
}
|
|
278
|
+
this.pushTag(this.lookahead);
|
|
279
|
+
return this.match(TOKEN_OPEN_TAG);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/*
|
|
283
|
+
* @return {[]Node}
|
|
284
|
+
*/
|
|
285
|
+
}, {
|
|
286
|
+
key: "elementOrData",
|
|
287
|
+
value: function elementOrData() {
|
|
288
|
+
var children = [];
|
|
289
|
+
if (this.predict(TOKEN_OPEN_TAG)) {
|
|
290
|
+
children.push(this.element());
|
|
291
|
+
children = children.concat(this.elementOrData());
|
|
292
|
+
}
|
|
293
|
+
if (this.predict(TOKEN_SELF_TAG, TOKEN_TEXT, TOKEN_PLACEHOLDER)) {
|
|
294
|
+
var token = this.match(this.lookahead.type);
|
|
295
|
+
var node;
|
|
296
|
+
switch (token.type) {
|
|
297
|
+
case TOKEN_SELF_TAG:
|
|
298
|
+
node = Node.createVoidNode(token);
|
|
299
|
+
break;
|
|
300
|
+
case TOKEN_TEXT:
|
|
301
|
+
node = Node.createTextNode(token);
|
|
302
|
+
break;
|
|
303
|
+
case TOKEN_PLACEHOLDER:
|
|
304
|
+
node = Node.createPlaceholderNode(token);
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
children.push(node);
|
|
308
|
+
children = children.concat(this.elementOrData());
|
|
309
|
+
}
|
|
310
|
+
return children;
|
|
311
|
+
}
|
|
312
|
+
}, {
|
|
313
|
+
key: "endTag",
|
|
314
|
+
value: function endTag() {
|
|
315
|
+
if (!this.predict(TOKEN_CLOSE_TAG)) {
|
|
316
|
+
throw new Error(SYNTAX_ERROR);
|
|
317
|
+
}
|
|
318
|
+
var openTag = this.tags.pop();
|
|
319
|
+
if (openTag.name !== this.lookahead.name) {
|
|
320
|
+
throw new Error(SYNTAX_ERROR);
|
|
321
|
+
}
|
|
322
|
+
this.match(TOKEN_CLOSE_TAG);
|
|
323
|
+
}
|
|
324
|
+
}, {
|
|
325
|
+
key: "lookahead",
|
|
326
|
+
get: function get() {
|
|
327
|
+
return this.tokens.length === 0 ? EPSILON : this.tokens[0];
|
|
328
|
+
}
|
|
329
|
+
}, {
|
|
330
|
+
key: "predict",
|
|
331
|
+
value: function predict() {
|
|
332
|
+
for (var _len = arguments.length, types = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
333
|
+
types[_key] = arguments[_key];
|
|
334
|
+
}
|
|
335
|
+
return types.includes(this.lookahead.type);
|
|
336
|
+
}
|
|
337
|
+
}, {
|
|
338
|
+
key: "match",
|
|
339
|
+
value: function match(type) {
|
|
340
|
+
if (!this.predict(type)) {
|
|
341
|
+
throw new Error(SYNTAX_ERROR);
|
|
342
|
+
}
|
|
343
|
+
var token = this.tokens.shift();
|
|
344
|
+
return token;
|
|
345
|
+
}
|
|
346
|
+
}, {
|
|
347
|
+
key: "pushTag",
|
|
348
|
+
value: function pushTag(token) {
|
|
349
|
+
this.tags.push(token);
|
|
350
|
+
}
|
|
351
|
+
}]);
|
|
352
|
+
return Parser;
|
|
353
|
+
}();
|
|
354
|
+
|
|
355
|
+
var createElement = function createElement(node, mapping, keyPrefix) {
|
|
356
|
+
var children = node.children.map(function (c, i) {
|
|
357
|
+
return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, {
|
|
358
|
+
key: keyPrefix + i
|
|
359
|
+
}, createElement(c, mapping, keyPrefix));
|
|
360
|
+
});
|
|
361
|
+
switch (node.type) {
|
|
362
|
+
case NODE_TEXT:
|
|
363
|
+
{
|
|
364
|
+
return node.text;
|
|
365
|
+
}
|
|
366
|
+
case NODE_FRAGMENT:
|
|
367
|
+
{
|
|
368
|
+
return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, children);
|
|
369
|
+
}
|
|
370
|
+
case NODE_VOID_ELEMENT:
|
|
371
|
+
{
|
|
372
|
+
var val = mapping[node.name];
|
|
373
|
+
if (typeof val === 'function') {
|
|
374
|
+
return val();
|
|
375
|
+
}
|
|
376
|
+
return val || /*#__PURE__*/React__default["default"].createElement(node.name, null);
|
|
377
|
+
}
|
|
378
|
+
case NODE_TAG_ELEMENT:
|
|
379
|
+
{
|
|
380
|
+
var _val = mapping[node.name];
|
|
381
|
+
if (_val === undefined) {
|
|
382
|
+
return /*#__PURE__*/React__default["default"].createElement(node.name, null, children);
|
|
383
|
+
}
|
|
384
|
+
if (typeof _val === 'function') {
|
|
385
|
+
return _val(children);
|
|
386
|
+
}
|
|
387
|
+
if (/*#__PURE__*/React__default["default"].isValidElement(_val)) {
|
|
388
|
+
if (React__default["default"].Children.count(_val.props.children) !== 0) {
|
|
389
|
+
throw new Error('when passing an element as value, the element should not contains children');
|
|
390
|
+
}
|
|
391
|
+
return /*#__PURE__*/React__default["default"].cloneElement(_val, {
|
|
392
|
+
children: children
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
throw new Error("Invalid mapping value for \"".concat(node.name, "\". Only element or render function are accepted"));
|
|
396
|
+
}
|
|
397
|
+
case NODE_PLACEHOLDER:
|
|
398
|
+
{
|
|
399
|
+
var _val2 = mapping[node.name];
|
|
400
|
+
if (_val2 === undefined) {
|
|
401
|
+
console.warn("missing \"".concat(node.name, "\" in mapping"));
|
|
402
|
+
return node.string;
|
|
403
|
+
}
|
|
404
|
+
if (typeof _val2 === 'function') {
|
|
405
|
+
return _val2();
|
|
406
|
+
}
|
|
407
|
+
return _val2;
|
|
408
|
+
}
|
|
409
|
+
default:
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
function Interpolate(_ref) {
|
|
414
|
+
var string = _ref.string,
|
|
415
|
+
syntax = _ref.syntax,
|
|
416
|
+
_ref$mapping = _ref.mapping,
|
|
417
|
+
mapping = _ref$mapping === void 0 ? {} : _ref$mapping,
|
|
418
|
+
_ref$graceful = _ref.graceful,
|
|
419
|
+
graceful = _ref$graceful === void 0 ? true : _ref$graceful;
|
|
420
|
+
try {
|
|
421
|
+
var tree = parser(string, syntax);
|
|
422
|
+
return createElement(tree, mapping, string);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
if (graceful) {
|
|
425
|
+
console.error(e);
|
|
426
|
+
return string;
|
|
427
|
+
} else {
|
|
428
|
+
throw e;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
Interpolate.propTypes = {
|
|
433
|
+
string: PropTypes__default["default"].string.isRequired,
|
|
434
|
+
mapping: PropTypes__default["default"].object,
|
|
435
|
+
graceful: PropTypes__default["default"].bool
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
exports.SYNTAX_BUILT_IN = SYNTAX_BUILT_IN;
|
|
439
|
+
exports.SYNTAX_I18NEXT = SYNTAX_I18NEXT;
|
|
440
|
+
exports.TOKEN_CLOSE_TAG = TOKEN_CLOSE_TAG;
|
|
441
|
+
exports.TOKEN_OPEN_TAG = TOKEN_OPEN_TAG;
|
|
442
|
+
exports.TOKEN_PLACEHOLDER = TOKEN_PLACEHOLDER;
|
|
443
|
+
exports.TOKEN_SELF_TAG = TOKEN_SELF_TAG;
|
|
444
|
+
exports["default"] = Interpolate;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const TOKEN_PLACEHOLDER: 'TOKEN_PLACEHOLDER'
|
|
2
|
+
|
|
3
|
+
type Syntax = {
|
|
4
|
+
type: typeof TOKEN_PLACEHOLDER
|
|
5
|
+
regex: RegExp
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type InterpolateProps = {
|
|
9
|
+
string: string
|
|
10
|
+
mapping: {
|
|
11
|
+
[key: string]: ((children: React.ReactNode) => React.ReactNode) | React.ReactNode
|
|
12
|
+
}
|
|
13
|
+
syntax?: Syntax[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare function Interpolate(props: InterpolateProps): JSX.Element
|
|
17
|
+
|
|
18
|
+
export = Interpolate
|