UNPKG

5.53 kBJavaScriptView Raw
1"use strict"
2
3const htmlparser = require("htmlparser2")
4const TransformableString = require("./TransformableString")
5
6function iterateScripts(code, options, onChunk) {
7 if (!code) return
8
9 const xmlMode = options.xmlMode
10 const isJavaScriptMIMEType = options.isJavaScriptMIMEType || (() => true)
11 let index = 0
12 let inScript = false
13 let cdata = []
14
15 const chunks = []
16 function pushChunk(type, end) {
17 chunks.push({ type, start: index, end, cdata })
18 cdata = []
19 index = end
20 }
21
22 const parser = new htmlparser.Parser(
23 {
24 onopentag(name, attrs) {
25 // Test if current tag is a valid <script> tag.
26 if (name !== "script") {
27 return
28 }
29
30 if (attrs.type && !isJavaScriptMIMEType(attrs.type)) {
31 return
32 }
33
34 inScript = true
35 pushChunk("html", parser.endIndex + 1)
36 },
37
38 oncdatastart() {
39 cdata.push(
40 {
41 start: parser.startIndex,
42 end: parser.startIndex + 9,
43 },
44 {
45 start: parser.endIndex - 2,
46 end: parser.endIndex + 1,
47 }
48 )
49 },
50
51 onclosetag(name) {
52 if (name !== "script" || !inScript) {
53 return
54 }
55
56 inScript = false
57
58 if (parser.startIndex < chunks[chunks.length - 1].end) {
59 // The parser didn't move its index after the previous chunk emited. It occurs on
60 // self-closing tags (xml mode). Just ignore this script.
61 return
62 }
63
64 pushChunk("script", parser.startIndex)
65 },
66
67 ontext() {
68 if (!inScript) {
69 return
70 }
71
72 pushChunk("script", parser.endIndex + 1)
73 },
74 },
75 {
76 xmlMode: xmlMode === true,
77 }
78 )
79
80 parser.parseComplete(code)
81
82 pushChunk("html", parser.endIndex + 1)
83
84 {
85 const emitChunk = () => {
86 const cdata = []
87 for (let i = startChunkIndex; i < index; i += 1) {
88 cdata.push.apply(cdata, chunks[i].cdata)
89 }
90 onChunk({
91 type: chunks[startChunkIndex].type,
92 start: chunks[startChunkIndex].start,
93 end: chunks[index - 1].end,
94 cdata,
95 })
96 }
97 let startChunkIndex = 0
98 let index
99 for (index = 1; index < chunks.length; index += 1) {
100 if (chunks[startChunkIndex].type === chunks[index].type) continue
101 emitChunk()
102 startChunkIndex = index
103 }
104
105 emitChunk()
106 }
107}
108
109function computeIndent(descriptor, previousHTML, slice) {
110 if (!descriptor) {
111 const indentMatch = /[\n\r]+([ \t]*)/.exec(slice)
112 return indentMatch ? indentMatch[1] : ""
113 }
114
115 if (descriptor.relative) {
116 return previousHTML.match(/([^\n\r]*)<[^<]*$/)[1] + descriptor.spaces
117 }
118
119 return descriptor.spaces
120}
121
122function* dedent(indent, slice) {
123 let hadNonEmptyLine = false
124 const re = /(\r\n|\n|\r)([ \t]*)(.*)/g
125 let lastIndex = 0
126
127 while (true) {
128 const match = re.exec(slice)
129 if (!match) break
130
131 const newLine = match[1]
132 const lineIndent = match[2]
133 const lineText = match[3]
134
135 const isEmptyLine = !lineText
136 const isFirstNonEmptyLine = !isEmptyLine && !hadNonEmptyLine
137
138 const badIndentation =
139 // Be stricter on the first line
140 isFirstNonEmptyLine
141 ? indent !== lineIndent
142 : lineIndent.indexOf(indent) !== 0
143
144 if (!badIndentation) {
145 lastIndex = match.index + newLine.length + indent.length
146 // Remove the first line if it is empty
147 const fromIndex = match.index === 0 ? 0 : match.index + newLine.length
148 yield {
149 type: "dedent",
150 from: fromIndex,
151 to: lastIndex,
152 }
153 }
154 else if (isEmptyLine) {
155 yield {
156 type: "empty",
157 }
158 }
159 else {
160 yield {
161 type: "bad-indent",
162 }
163 }
164
165 if (!isEmptyLine) {
166 hadNonEmptyLine = true
167 }
168 }
169
170 const endSpaces = slice.slice(lastIndex).match(/[ \t]*$/)[0].length
171 if (endSpaces) {
172 yield {
173 type: "dedent",
174 from: slice.length - endSpaces,
175 to: slice.length,
176 }
177 }
178}
179
180function extract(code, indentDescriptor, xmlMode, isJavaScriptMIMEType) {
181 const badIndentationLines = []
182 const codeParts = []
183 let lineNumber = 1
184 let previousHTML = ""
185
186 iterateScripts(code, { xmlMode, isJavaScriptMIMEType }, (chunk) => {
187 const slice = code.slice(chunk.start, chunk.end)
188 if (chunk.type === "html") {
189 const match = slice.match(/\r\n|\n|\r/g)
190 if (match) lineNumber += match.length
191 previousHTML = slice
192 }
193 else if (chunk.type === "script") {
194 const transformedCode = new TransformableString(code)
195 let indentSlice = slice
196 for (const cdata of chunk.cdata) {
197 transformedCode.replace(cdata.start, cdata.end, "")
198 if (cdata.end === chunk.end) {
199 indentSlice = code.slice(chunk.start, cdata.start)
200 }
201 }
202 transformedCode.replace(0, chunk.start, "")
203 transformedCode.replace(chunk.end, code.length, "")
204 for (const action of dedent(
205 computeIndent(indentDescriptor, previousHTML, indentSlice),
206 indentSlice
207 )) {
208 lineNumber += 1
209 if (action.type === "dedent") {
210 transformedCode.replace(
211 chunk.start + action.from,
212 chunk.start + action.to,
213 ""
214 )
215 }
216 else if (action.type === "bad-indent") {
217 badIndentationLines.push(lineNumber)
218 }
219 }
220 codeParts.push(transformedCode)
221 }
222 })
223
224 return {
225 code: codeParts,
226 badIndentationLines,
227 }
228}
229
230module.exports = extract