-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfix-whitespace.js
242 lines (196 loc) · 6.42 KB
/
fix-whitespace.js
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
'use strict'
function tag(literals, ...values) {
// Tag function used with template strings that applies various convenient
// modifications to multiline strings, including:
//
// * Joins literals and embedded values of a template string while keeping
// the indentation from the literals (see joinTemplateString)
//
// * Removes the greatest amount of indentation from the beginning of each
// line, such that the indentation differences between each line are kept
// (see whitespaceAtBeginningMultiline)
//
// * Removes whitespace-only lines at the beginning and end of the string
// (see removeInitialWhitespaceLines and removeLeadingWhitespaceLines)
let resultLines = joinTemplateString(literals, ...values).split('\n')
// Reduce whitespace from beginning of lines
const minWhitespace = whitespaceAtBeginningMultiline(literals.join(''))
resultLines = resultLines.map(line => line.slice(minWhitespace))
// Remove whitespace lines at beginning and end
let result = resultLines.join('\n')
result = removeInitialWhitespaceLines(result)
result = removeLeadingWhitespaceLines(result)
return result
}
function joinTemplateString(literals, ...values) {
// Joins the literals and values of a template string while maintaining
// indentation passed from literals. For example, using this template:
//
// `
// <ul>
// ${items.map(item => `<li>${item}</li>`).join('\n')}
// </ul>
// `
//
// ..and given the array ['A', 'B', 'C'] for items, this result would be
// gotten:
//
// `
// <ul>
// <li>A</li>
// <li>B</li>
// <li>C</li>
// </ul>
// `
let resultLines = []
for (let literalI = 0; literalI < literals.length; literalI++) {
const literal = literals[literalI]
const literalLines = literal.split('\n')
let curResultLines = literalLines.slice(0)
const value = values[literalI]
if (value) {
const lastLiteralLine = literalLines[literalLines.length - 1]
const lastResultLine = resultLines[resultLines.length - 1] || ''
// Normally we just add the whitespace from the last line of the current
// literal. However, if there are multiple literals corresponding to a
// single line of the tag input string, we want to use the whitespace
// from the latest result line - because the literal won't contain any
// of the result's indent!
const whitespaceAmountLit = whitespaceAtBeginning(lastLiteralLine)
const whitespaceAmountRes = whitespaceAtBeginning(lastResultLine)
const whitespaceAmountMax = Math.max(whitespaceAmountLit, whitespaceAmountRes)
const [first, ...rest] = value.toString().split('\n')
const modified = [first, ...rest.map(
line => ' '.repeat(whitespaceAmountMax) + line
)]
curResultLines = squishArrays(curResultLines, modified)
}
resultLines = squishArrays(resultLines, curResultLines)
}
return resultLines.join('\n')
}
function removeInitialWhitespaceLines(lines) {
// Removes whitespace-only lines from the beginning of a string.
const result = lines.split('\n')
while (result.length) {
if (isOnlyWhitespace(result[0])) {
result.shift()
} else {
break
}
}
return result.join('\n')
}
function removeLeadingWhitespaceLines(lines) {
// Removes whitespace-only lines from the ending of a string.
const result = lines.split('\n')
while (result.length) {
if (isOnlyWhitespace(result[result.length - 1])) {
result.pop()
} else {
break
}
}
return result.join('\n')
}
function squishArrays(arr1, arr2) {
// "Squishes" two string arrays together. Basically works like a concat, but
// with the first item of arr2 being concatenated to the last item of arr1.
const [first, ...rest] = arr2
const result = arr1.slice(0, -1)
const lastOfArr1 = arr1[arr1.length - 1]
result.push((lastOfArr1 || '') + first)
result.push(...rest)
return result
}
function whitespaceAtBeginningMultiline(str) {
// Gets the minimum number of whitespace characters at the beginning of each
// of the lines of a multiline string. For example, in this string:
//
// `
// Hello
// World
// !!!!!
// `
//
// The value 1 would be returned, for the one whitespace before "!!!!!".
//
// Note that this function ignores whitespace-only lines. Called on this
// string, where each dot is a space, it will return the value 4:
//
// `
// ....Hi
// .......There
// ..
// ....Friendo!
// `
//
// Though the amount of whitespace on the third line is 2, the function
// ignores this line, since it has no non-whitespace characters.
//
// In a string composed entirely of whitespace, the longest line length
// is returned.
const lines = str.split('\n')
return lines.reduce(
(min, line) => {
if (isOnlyWhitespace(line)) {
return min
} else {
return Math.min(min, whitespaceAtBeginning(line))
}
},
Math.max(...lines.map(line => line.length))
)
}
function isOnlyWhitespace(str) {
// Returns whether a passed string is composed solely of whitespace.
// Works only on single-line strings, as newlines are not considered
// whitespace.
return whitespaceAtBeginning(str) === str.length
}
function whitespaceAtBeginning(str) {
// Gets the number of whitespace characters at the beginning of a string.
for (let i = 0; i < str.length; i++) {
if (!isWhitespaceCharacter(str[i])) {
return i
}
}
return str.length
}
function isWhitespaceCharacter(char) {
// Returns whether a passed character is a whitespace character or not.
// May be expanded. Intentionally does not include line break characters.
return [' '].includes(char)
}
if (require.main === module) {
const items = [1, 2, 3]
const generateSitePage = (head, content) => tag`
<!DOCTYPE html>
<html>
<head>
(Begin head..)${head}(..End head)
<meta charset='utf-8'>
</head>
<body>
<div id='content'>
${"Early insert!"} (Begin content x1..)${content}(..end content; begin content x2..)${content}(..end content!)
${"And back down."}
</div>
</body>
</html>
`
console.log(generateSitePage(
tag`
<title>Hello, world!</title>
`,
tag`
<ul>
${items.map(x => `<li>${x}</li>`).join('\n')}
</ul>
<!-- Further indent at end! -->
`
))
}
if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = tag
}