@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/src/lexer.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TOKEN_TEXT } from './constants'
|
|
2
|
+
export { SYNTAX_I18NEXT, SYNTAX_BUILT_IN } from './syntax'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* return a array of token object
|
|
6
|
+
*/
|
|
7
|
+
export default function lexer(string, syntax) {
|
|
8
|
+
const matches = []
|
|
9
|
+
|
|
10
|
+
for (const rule of syntax) {
|
|
11
|
+
const { type, regex } = rule
|
|
12
|
+
|
|
13
|
+
var res
|
|
14
|
+
while ((res = regex.exec(string)) !== null) {
|
|
15
|
+
matches.push({
|
|
16
|
+
type,
|
|
17
|
+
string: res[0],
|
|
18
|
+
name: res[1],
|
|
19
|
+
start: res.index,
|
|
20
|
+
end: res.index + res[0].length,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
matches.sort((a, b) => a.start - b.start)
|
|
26
|
+
const textTokens = []
|
|
27
|
+
|
|
28
|
+
let start = 0
|
|
29
|
+
matches.forEach((match) => {
|
|
30
|
+
if (match.start === start) {
|
|
31
|
+
start = match.end
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const end = match.start
|
|
36
|
+
const text = string.substring(start, end)
|
|
37
|
+
|
|
38
|
+
textTokens.push({
|
|
39
|
+
type: TOKEN_TEXT,
|
|
40
|
+
string: text,
|
|
41
|
+
text,
|
|
42
|
+
start,
|
|
43
|
+
end,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
start = match.end
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const text = string.substring(start)
|
|
50
|
+
if (text !== '') {
|
|
51
|
+
textTokens.push({
|
|
52
|
+
type: TOKEN_TEXT,
|
|
53
|
+
string: text,
|
|
54
|
+
start,
|
|
55
|
+
end: string.length,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tokens = [].concat(textTokens, matches).sort((a, b) => a.start - b.start)
|
|
60
|
+
|
|
61
|
+
return tokens
|
|
62
|
+
}
|
package/src/node.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NODE_FRAGMENT,
|
|
3
|
+
NODE_TAG_ELEMENT,
|
|
4
|
+
NODE_VOID_ELEMENT,
|
|
5
|
+
NODE_PLACEHOLDER,
|
|
6
|
+
NODE_TEXT,
|
|
7
|
+
} from './constants'
|
|
8
|
+
|
|
9
|
+
export default class Node {
|
|
10
|
+
constructor({ type, children, ...fields }) {
|
|
11
|
+
this.type = type
|
|
12
|
+
this.children = children || []
|
|
13
|
+
|
|
14
|
+
for (const key in fields) {
|
|
15
|
+
this[key] = fields[key]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (fields.token) {
|
|
19
|
+
this.string = fields.token.string
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
appendChild(child) {
|
|
24
|
+
this.children.push(child)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get isLeaf() {
|
|
28
|
+
return this.children.length === 0
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Node.createTagNode = (token, children) =>
|
|
33
|
+
new Node({
|
|
34
|
+
type: NODE_TAG_ELEMENT,
|
|
35
|
+
children,
|
|
36
|
+
name: token.name,
|
|
37
|
+
token,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
Node.createFragmentNode = (children) =>
|
|
41
|
+
new Node({
|
|
42
|
+
type: NODE_FRAGMENT,
|
|
43
|
+
children,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
Node.createVoidNode = (token) =>
|
|
47
|
+
new Node({
|
|
48
|
+
type: NODE_VOID_ELEMENT,
|
|
49
|
+
name: token.name,
|
|
50
|
+
token,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
Node.createTextNode = (token) =>
|
|
54
|
+
new Node({
|
|
55
|
+
type: NODE_TEXT,
|
|
56
|
+
text: token.string,
|
|
57
|
+
token,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
Node.createPlaceholderNode = (token) =>
|
|
61
|
+
new Node({
|
|
62
|
+
type: NODE_PLACEHOLDER,
|
|
63
|
+
name: token.name,
|
|
64
|
+
token,
|
|
65
|
+
})
|
package/src/parser.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TOKEN_PLACEHOLDER,
|
|
3
|
+
TOKEN_OPEN_TAG,
|
|
4
|
+
TOKEN_CLOSE_TAG,
|
|
5
|
+
TOKEN_SELF_TAG,
|
|
6
|
+
TOKEN_TEXT,
|
|
7
|
+
} from './constants'
|
|
8
|
+
import Node from './node.js'
|
|
9
|
+
import lexer from './lexer'
|
|
10
|
+
import { SYNTAX_BUILT_IN } from './syntax'
|
|
11
|
+
|
|
12
|
+
export default function parser(string, syntax) {
|
|
13
|
+
if (!syntax) {
|
|
14
|
+
syntax = SYNTAX_BUILT_IN
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const tokens = lexer(string, syntax)
|
|
18
|
+
const p = new Parser(tokens)
|
|
19
|
+
return p.parse()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SYNTAX_ERROR = 'Syntax error. Please check if each open tag is closed correctly'
|
|
23
|
+
|
|
24
|
+
// A special token representing end of the tream
|
|
25
|
+
const EPSILON = {
|
|
26
|
+
type: 'EPSILON',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
* A recursive descent parser that construct a syntax tree represeting
|
|
31
|
+
* the DOM hierarchy structure.
|
|
32
|
+
*
|
|
33
|
+
* For instance, the following token
|
|
34
|
+
* <h1>hi <b>{name}</b></h1>
|
|
35
|
+
*
|
|
36
|
+
* Would result in the following tree
|
|
37
|
+
*
|
|
38
|
+
* <h1>
|
|
39
|
+
* ├─ "hi"
|
|
40
|
+
* └─ <b>
|
|
41
|
+
* ├─ {name}
|
|
42
|
+
*
|
|
43
|
+
* The parser is based on the following grammar:
|
|
44
|
+
*
|
|
45
|
+
* document ::= elementOrData
|
|
46
|
+
* elementOrData ::= element elementOrData
|
|
47
|
+
* elementOrData ::= data elementOrData
|
|
48
|
+
* element ::= <open_tag> elementOrData <close_tag>
|
|
49
|
+
* data ::= <selfTag>
|
|
50
|
+
* data ::= <text>
|
|
51
|
+
* data ::= <placeholder>
|
|
52
|
+
*
|
|
53
|
+
* ref: https://github.com/amghannam/Mini-XML-Recursive-Descent-Parser
|
|
54
|
+
*/
|
|
55
|
+
class Parser {
|
|
56
|
+
constructor(tokens) {
|
|
57
|
+
this.tokens = [].concat(tokens)
|
|
58
|
+
this.tags = []
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
parse() {
|
|
62
|
+
const tree = this.document()
|
|
63
|
+
if (!this.predict(EPSILON.type)) {
|
|
64
|
+
throw new Error(SYNTAX_ERROR)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return tree
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
* @return {Node}
|
|
72
|
+
*/
|
|
73
|
+
document() {
|
|
74
|
+
const children = this.elementOrData()
|
|
75
|
+
return Node.createFragmentNode(children)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* @return {Node}
|
|
80
|
+
*/
|
|
81
|
+
element() {
|
|
82
|
+
const tagToken = this.openTag()
|
|
83
|
+
const children = this.elementOrData()
|
|
84
|
+
this.endTag()
|
|
85
|
+
|
|
86
|
+
return Node.createTagNode(tagToken, children)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/*
|
|
90
|
+
* @return {Token}
|
|
91
|
+
*/
|
|
92
|
+
openTag() {
|
|
93
|
+
if (!this.predict(TOKEN_OPEN_TAG)) {
|
|
94
|
+
throw new Error(SYNTAX_ERROR)
|
|
95
|
+
}
|
|
96
|
+
this.pushTag(this.lookahead)
|
|
97
|
+
return this.match(TOKEN_OPEN_TAG)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/*
|
|
101
|
+
* @return {[]Node}
|
|
102
|
+
*/
|
|
103
|
+
elementOrData() {
|
|
104
|
+
let children = []
|
|
105
|
+
|
|
106
|
+
if (this.predict(TOKEN_OPEN_TAG)) {
|
|
107
|
+
children.push(this.element())
|
|
108
|
+
children = children.concat(this.elementOrData())
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.predict(TOKEN_SELF_TAG, TOKEN_TEXT, TOKEN_PLACEHOLDER)) {
|
|
112
|
+
const token = this.match(this.lookahead.type)
|
|
113
|
+
|
|
114
|
+
let node
|
|
115
|
+
switch (token.type) {
|
|
116
|
+
case TOKEN_SELF_TAG:
|
|
117
|
+
node = Node.createVoidNode(token)
|
|
118
|
+
break
|
|
119
|
+
case TOKEN_TEXT:
|
|
120
|
+
node = Node.createTextNode(token)
|
|
121
|
+
break
|
|
122
|
+
case TOKEN_PLACEHOLDER:
|
|
123
|
+
node = Node.createPlaceholderNode(token)
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
children.push(node)
|
|
128
|
+
children = children.concat(this.elementOrData())
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return children
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
endTag() {
|
|
135
|
+
if (!this.predict(TOKEN_CLOSE_TAG)) {
|
|
136
|
+
throw new Error(SYNTAX_ERROR)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const openTag = this.tags.pop()
|
|
140
|
+
if (openTag.name !== this.lookahead.name) {
|
|
141
|
+
throw new Error(SYNTAX_ERROR)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.match(TOKEN_CLOSE_TAG)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get lookahead() {
|
|
148
|
+
return this.tokens.length === 0 ? EPSILON : this.tokens[0]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
predict(...types) {
|
|
152
|
+
return types.includes(this.lookahead.type)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
match(type) {
|
|
156
|
+
if (!this.predict(type)) {
|
|
157
|
+
throw new Error(SYNTAX_ERROR)
|
|
158
|
+
}
|
|
159
|
+
const token = this.tokens.shift()
|
|
160
|
+
return token
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pushTag(token) {
|
|
164
|
+
this.tags.push(token)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -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
|
package/src/syntax.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TOKEN_PLACEHOLDER, TOKEN_OPEN_TAG, TOKEN_CLOSE_TAG, TOKEN_SELF_TAG } from './constants'
|
|
2
|
+
|
|
3
|
+
export const SYNTAX_BUILT_IN = [
|
|
4
|
+
{
|
|
5
|
+
type: TOKEN_PLACEHOLDER,
|
|
6
|
+
regex: /{\s*(\w+)\s*}/g,
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
type: TOKEN_OPEN_TAG,
|
|
10
|
+
regex: /<(\w+)>/g,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
type: TOKEN_CLOSE_TAG,
|
|
14
|
+
regex: /<\/(\w+)>/g,
|
|
15
|
+
},
|
|
16
|
+
{ type: TOKEN_SELF_TAG, regex: /<(\w+)\s*\/>/g },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export const SYNTAX_I18NEXT = [
|
|
20
|
+
{
|
|
21
|
+
type: TOKEN_PLACEHOLDER,
|
|
22
|
+
regex: /{{\s*(\w+)\s*}}/g,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: TOKEN_OPEN_TAG,
|
|
26
|
+
regex: /<(\w+)>/g,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: TOKEN_CLOSE_TAG,
|
|
30
|
+
regex: /<\/(\w+)>/g,
|
|
31
|
+
},
|
|
32
|
+
{ type: TOKEN_SELF_TAG, regex: /<(\w+)\s*\/>/g },
|
|
33
|
+
]
|