1 | "use strict"
|
2 |
|
3 | const htmlparser = require("htmlparser2")
|
4 | const TransformableString = require("./TransformableString")
|
5 |
|
6 | function 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 |
|
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 |
|
60 |
|
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 |
|
109 | function 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 |
|
122 | function* 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 |
|
140 | isFirstNonEmptyLine
|
141 | ? indent !== lineIndent
|
142 | : lineIndent.indexOf(indent) !== 0
|
143 |
|
144 | if (!badIndentation) {
|
145 | lastIndex = match.index + newLine.length + indent.length
|
146 |
|
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 |
|
180 | function 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 |
|
230 | module.exports = extract
|