-
-
Notifications
You must be signed in to change notification settings - Fork 60
/
parse.ts
198 lines (160 loc) · 5.61 KB
/
parse.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import { ParseErr } from "./err.ts";
import { trimWS } from "./utils.ts";
/* TYPES */
import type { EtaConfig } from "./config.ts";
export type TagType = "r" | "e" | "i" | "";
export interface TemplateObject {
t: TagType;
val: string;
}
export type AstObject = string | TemplateObject;
/* END TYPES */
var templateLitReg =
/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})*}|(?!\${)[^\\`])*`/g;
var singleQuoteReg = /'(?:\\[\s\w"'\\`]|[^\n\r'\\])*?'/g;
var doubleQuoteReg = /"(?:\\[\s\w"'\\`]|[^\n\r"\\])*?"/g;
/** Escape special regular expression characters inside a string */
function escapeRegExp(string: string) {
// From MDN
return string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
export default function parse(
str: string,
config: EtaConfig,
): Array<AstObject> {
var buffer: Array<AstObject> = [];
var trimLeftOfNextStr: string | false = false;
var lastIndex = 0;
var parseOptions = config.parse;
/* Adding for EJS compatibility */
if (config.rmWhitespace) {
// Code taken directly from EJS
// Have to use two separate replaces here as `^` and `$` operators don't
// work well with `\r` and empty lines don't work well with the `m` flag.
// Essentially, this replaces the whitespace at the beginning and end of
// each line and removes multiple newlines.
str = str.replace(/[\r\n]+/g, "\n").replace(/^\s+|\s+$/gm, "");
}
/* End rmWhitespace option */
templateLitReg.lastIndex = 0;
singleQuoteReg.lastIndex = 0;
doubleQuoteReg.lastIndex = 0;
function pushString(strng: string, shouldTrimRightOfString?: string | false) {
if (strng) {
// if string is truthy it must be of type 'string'
strng = trimWS(
strng,
config,
trimLeftOfNextStr, // this will only be false on the first str, the next ones will be null or undefined
shouldTrimRightOfString,
);
if (strng) {
// replace \ with \\, ' with \'
// we're going to convert all CRLF to LF so it doesn't take more than one replace
strng = strng.replace(/\\|'/g, "\\$&").replace(/\r\n|\n|\r/g, "\\n");
buffer.push(strng);
}
}
}
var prefixes = [parseOptions.exec, parseOptions.interpolate, parseOptions.raw]
.reduce(function (
accumulator,
prefix,
) {
if (accumulator && prefix) {
return accumulator + "|" + escapeRegExp(prefix);
} else if (prefix) {
// accumulator is falsy
return escapeRegExp(prefix);
} else {
// prefix and accumulator are both falsy
return accumulator;
}
}, "");
var parseOpenReg = new RegExp(
"([^]*?)" + escapeRegExp(config.tags[0]) + "(-|_)?\\s*(" + prefixes +
")?\\s*",
"g",
);
var parseCloseReg = new RegExp(
"'|\"|`|\\/\\*|(\\s*(-|_)?" + escapeRegExp(config.tags[1]) + ")",
"g",
);
// TODO: benchmark having the \s* on either side vs using str.trim()
var m;
while ((m = parseOpenReg.exec(str))) {
lastIndex = m[0].length + m.index;
var precedingString = m[1];
var wsLeft = m[2];
var prefix = m[3] || ""; // by default either ~, =, or empty
pushString(precedingString, wsLeft);
parseCloseReg.lastIndex = lastIndex;
var closeTag;
var currentObj: AstObject | false = false;
while ((closeTag = parseCloseReg.exec(str))) {
if (closeTag[1]) {
var content = str.slice(lastIndex, closeTag.index);
parseOpenReg.lastIndex = lastIndex = parseCloseReg.lastIndex;
trimLeftOfNextStr = closeTag[2];
var currentType: TagType = "";
if (prefix === parseOptions.exec) {
currentType = "e";
} else if (prefix === parseOptions.raw) {
currentType = "r";
} else if (prefix === parseOptions.interpolate) {
currentType = "i";
}
currentObj = { t: currentType, val: content };
break;
} else {
var char = closeTag[0];
if (char === "/*") {
var commentCloseInd = str.indexOf("*/", parseCloseReg.lastIndex);
if (commentCloseInd === -1) {
ParseErr("unclosed comment", str, closeTag.index);
}
parseCloseReg.lastIndex = commentCloseInd;
} else if (char === "'") {
singleQuoteReg.lastIndex = closeTag.index;
var singleQuoteMatch = singleQuoteReg.exec(str);
if (singleQuoteMatch) {
parseCloseReg.lastIndex = singleQuoteReg.lastIndex;
} else {
ParseErr("unclosed string", str, closeTag.index);
}
} else if (char === '"') {
doubleQuoteReg.lastIndex = closeTag.index;
var doubleQuoteMatch = doubleQuoteReg.exec(str);
if (doubleQuoteMatch) {
parseCloseReg.lastIndex = doubleQuoteReg.lastIndex;
} else {
ParseErr("unclosed string", str, closeTag.index);
}
} else if (char === "`") {
templateLitReg.lastIndex = closeTag.index;
var templateLitMatch = templateLitReg.exec(str);
if (templateLitMatch) {
parseCloseReg.lastIndex = templateLitReg.lastIndex;
} else {
ParseErr("unclosed string", str, closeTag.index);
}
}
}
}
if (currentObj) {
buffer.push(currentObj);
} else {
ParseErr("unclosed tag", str, m.index + precedingString.length);
}
}
pushString(str.slice(lastIndex, str.length), false);
if (config.plugins) {
for (var i = 0; i < config.plugins.length; i++) {
var plugin = config.plugins[i];
if (plugin.processAST) {
buffer = plugin.processAST(buffer, config);
}
}
}
return buffer;
}