• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eta-dev / eta / 10292313621

07 Aug 2024 09:56PM UTC coverage: 95.552%. Remained the same
10292313621

push

github

nebrelbug
3.4.1

216 of 232 branches covered (93.1%)

Branch coverage included in aggregate %.

321 of 330 relevant lines covered (97.27%)

57.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.24
/src/parse.ts
1
import { ParseErr } from "./err.ts";
16✔
2
import { trimWS } from "./utils.ts";
16✔
3

4
/* TYPES */
5

6
import type { Eta } from "./core.ts";
7

8
export type TagType = "r" | "e" | "i" | "";
9

10
export interface TemplateObject {
11
  t: TagType;
12
  val: string;
13
  lineNo?: number;
14
}
15

16
export type AstObject = string | TemplateObject;
17

18
/* END TYPES */
19

20
const templateLitReg =
21
  /`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})*}|(?!\${)[^\\`])*`/g;
16✔
22

23
const singleQuoteReg = /'(?:\\[\s\w"'\\`]|[^\n\r'\\])*?'/g;
16✔
24

25
const doubleQuoteReg = /"(?:\\[\s\w"'\\`]|[^\n\r"\\])*?"/g;
16✔
26

27
/** Escape special regular expression characters inside a string */
28

29
function escapeRegExp(string: string) {
30
  // From MDN
31
  return string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
456✔
32
}
33

34
function getLineNo(str: string, index: number) {
35
  return str.slice(0, index).split("\n").length;
2✔
36
}
37

38
export function parse(this: Eta, str: string): Array<AstObject> {
16✔
39
  const config = this.config;
114✔
40

41
  let buffer: Array<AstObject> = [];
114✔
42
  let trimLeftOfNextStr: string | false = false;
114✔
43
  let lastIndex = 0;
114✔
44
  const parseOptions = config.parse;
114✔
45

46
  if (config.plugins) {
114✔
47
    for (let i = 0; i < config.plugins.length; i++) {
114✔
48
      const plugin = config.plugins[i];
10✔
49
      if (plugin.processTemplate) {
10✔
50
        str = plugin.processTemplate(str, config);
8✔
51
      }
52
    }
53
  }
54

55
  /* Adding for EJS compatibility */
56
  if (config.rmWhitespace) {
114!
57
    // Code taken directly from EJS
58
    // Have to use two separate replaces here as `^` and `$` operators don't
59
    // work well with `\r` and empty lines don't work well with the `m` flag.
60
    // Essentially, this replaces the whitespace at the beginning and end of
61
    // each line and removes multiple newlines.
62
    str = str.replace(/[\r\n]+/g, "\n").replace(/^\s+|\s+$/gm, "");
×
63
  }
64
  /* End rmWhitespace option */
65

66
  templateLitReg.lastIndex = 0;
114✔
67
  singleQuoteReg.lastIndex = 0;
114✔
68
  doubleQuoteReg.lastIndex = 0;
114✔
69

70
  function pushString(strng: string, shouldTrimRightOfString?: string | false) {
71
    if (strng) {
304✔
72
      // if string is truthy it must be of type 'string'
73

74
      strng = trimWS(
190✔
75
        strng,
76
        config,
77
        trimLeftOfNextStr, // this will only be false on the first str, the next ones will be null or undefined
78
        shouldTrimRightOfString,
79
      );
80

81
      if (strng) {
190✔
82
        // replace \ with \\, ' with \'
83
        // we're going to convert all CRLF to LF so it doesn't take more than one replace
84

85
        strng = strng.replace(/\\|'/g, "\\$&").replace(/\r\n|\n|\r/g, "\\n");
162✔
86

87
        buffer.push(strng);
162✔
88
      }
89
    }
90
  }
91

92
  const prefixes = [
114✔
93
    parseOptions.exec,
94
    parseOptions.interpolate,
95
    parseOptions.raw,
96
  ].reduce(function (
97
    accumulator,
98
    prefix,
99
  ) {
100
    if (accumulator && prefix) {
342✔
101
      return accumulator + "|" + escapeRegExp(prefix);
114✔
102
    } else if (prefix) {
228✔
103
      // accumulator is falsy
104
      return escapeRegExp(prefix);
114✔
105
    } else {
106
      // prefix and accumulator are both falsy
107
      return accumulator;
114✔
108
    }
109
  }, "");
110

111
  const parseOpenReg = new RegExp(
114✔
112
    escapeRegExp(config.tags[0]) + "(-|_)?\\s*(" + prefixes + ")?\\s*",
113
    "g",
114
  );
115

116
  const parseCloseReg = new RegExp(
114✔
117
    "'|\"|`|\\/\\*|(\\s*(-|_)?" + escapeRegExp(config.tags[1]) + ")",
118
    "g",
119
  );
120

121
  let m;
122

123
  while ((m = parseOpenReg.exec(str))) {
114✔
124
    const precedingString = str.slice(lastIndex, m.index);
204✔
125

126
    lastIndex = m[0].length + m.index;
204✔
127

128
    const wsLeft = m[1];
204✔
129
    const prefix = m[2] || ""; // by default either ~, =, or empty
204✔
130

131
    pushString(precedingString, wsLeft);
204✔
132

133
    parseCloseReg.lastIndex = lastIndex;
204✔
134
    let closeTag;
135
    let currentObj: AstObject | false = false;
204✔
136

137
    while ((closeTag = parseCloseReg.exec(str))) {
204✔
138
      if (closeTag[1]) {
244✔
139
        const content = str.slice(lastIndex, closeTag.index);
190✔
140

141
        parseOpenReg.lastIndex = lastIndex = parseCloseReg.lastIndex;
190✔
142

143
        trimLeftOfNextStr = closeTag[2];
190✔
144

145
        const currentType: TagType = prefix === parseOptions.exec
190✔
146
          ? "e"
147
          : prefix === parseOptions.raw
65✔
148
          ? "r"
149
          : prefix === parseOptions.interpolate
51!
150
          ? "i"
151
          : "";
152

153
        currentObj = { t: currentType, val: content };
190✔
154
        break;
190✔
155
      } else {
156
        const char = closeTag[0];
54✔
157
        if (char === "/*") {
54✔
158
          const commentCloseInd = str.indexOf("*/", parseCloseReg.lastIndex);
4✔
159

160
          if (commentCloseInd === -1) {
4✔
161
            ParseErr("unclosed comment", str, closeTag.index);
2✔
162
          }
163
          parseCloseReg.lastIndex = commentCloseInd;
2✔
164
        } else if (char === "'") {
50✔
165
          singleQuoteReg.lastIndex = closeTag.index;
22✔
166

167
          const singleQuoteMatch = singleQuoteReg.exec(str);
22✔
168
          if (singleQuoteMatch) {
22✔
169
            parseCloseReg.lastIndex = singleQuoteReg.lastIndex;
20✔
170
          } else {
171
            ParseErr("unclosed string", str, closeTag.index);
2✔
172
          }
173
        } else if (char === '"') {
28✔
174
          doubleQuoteReg.lastIndex = closeTag.index;
24✔
175
          const doubleQuoteMatch = doubleQuoteReg.exec(str);
24✔
176

177
          if (doubleQuoteMatch) {
24✔
178
            parseCloseReg.lastIndex = doubleQuoteReg.lastIndex;
22✔
179
          } else {
180
            ParseErr("unclosed string", str, closeTag.index);
2✔
181
          }
182
        } else if (char === "`") {
4✔
183
          templateLitReg.lastIndex = closeTag.index;
4✔
184
          const templateLitMatch = templateLitReg.exec(str);
4✔
185
          if (templateLitMatch) {
4✔
186
            parseCloseReg.lastIndex = templateLitReg.lastIndex;
2✔
187
          } else {
188
            ParseErr("unclosed string", str, closeTag.index);
2✔
189
          }
190
        }
191
      }
192
    }
193
    if (currentObj) {
196✔
194
      if (config.debug) {
190✔
195
        currentObj.lineNo = getLineNo(str, m.index);
2✔
196
      }
197
      buffer.push(currentObj);
190✔
198
    } else {
199
      ParseErr("unclosed tag", str, m.index);
6✔
200
    }
201
  }
202

203
  pushString(str.slice(lastIndex, str.length), false);
100✔
204

205
  if (config.plugins) {
100✔
206
    for (let i = 0; i < config.plugins.length; i++) {
100✔
207
      const plugin = config.plugins[i];
10✔
208
      if (plugin.processAST) {
10✔
209
        buffer = plugin.processAST(buffer, config);
2✔
210
      }
211
    }
212
  }
213

214
  return buffer;
100✔
215
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc