@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/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
+ ]