fwtoolkit 0.1.0-alpha.1 → 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -2
- package/src/basic.js +500 -0
- package/src/blob.js +1 -1
- package/src/content_menu.js +475 -0
- package/src/datatable_bulk.js +208 -0
- package/src/dialog.js +484 -0
- package/src/events.js +9 -0
- package/src/faq_dialog.js +67 -0
- package/src/file/dialog.js +142 -0
- package/src/file/index.js +9 -0
- package/src/file/new_folder_dialog.js +37 -0
- package/src/file/selector.js +263 -0
- package/src/file/templates.js +11 -0
- package/src/file/tools.js +58 -0
- package/src/focus.js +20 -0
- package/src/index.js +61 -12
- package/src/network.js +63 -14
- package/src/overview_menu.js +611 -0
- package/src/settings.js +18 -0
- package/src/templates.js +42 -0
- package/src/user.js +46 -0
- package/src/user_util.js +16 -0
- package/src/worker.js +12 -0
- package/src/ws.js +347 -0
- package/src/file.js +0 -25
- package/src/text.js +0 -44
package/src/user.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {post} from "./network"
|
|
2
|
+
|
|
3
|
+
export const setLanguage = (_config, language) =>
|
|
4
|
+
post("/api/i18n/setlang/", {language}).then(() => {
|
|
5
|
+
// We delete the network cache as this contains the JS
|
|
6
|
+
// translations.
|
|
7
|
+
caches.keys().then(names => {
|
|
8
|
+
for (const name of names) {
|
|
9
|
+
caches.delete(name)
|
|
10
|
+
}
|
|
11
|
+
window.location.reload()
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const COLOR_CACHE = {}
|
|
16
|
+
|
|
17
|
+
const userColor = string => {
|
|
18
|
+
// Source https://gist.github.com/0x263b/2bdd90886c2036a1ad5bcf06d6e6fb37
|
|
19
|
+
if (string.length === 0) {
|
|
20
|
+
return "rgb(0,0,0)"
|
|
21
|
+
} else if (COLOR_CACHE[string]) {
|
|
22
|
+
return COLOR_CACHE[string]
|
|
23
|
+
}
|
|
24
|
+
let hash = 0
|
|
25
|
+
for (let i = 0; i < string.length; i++) {
|
|
26
|
+
hash = string.charCodeAt(i) + ((hash << 5) - hash)
|
|
27
|
+
hash = hash & hash
|
|
28
|
+
}
|
|
29
|
+
const rgb = [0, 0, 0]
|
|
30
|
+
for (let i = 0; i < 3; i++) {
|
|
31
|
+
rgb[i] = (hash >> (i * 8)) & 255
|
|
32
|
+
}
|
|
33
|
+
COLOR_CACHE[string] = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`
|
|
34
|
+
return COLOR_CACHE[string]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A template for the default round avatar view. */
|
|
38
|
+
export const avatarTemplate = ({user}) => {
|
|
39
|
+
const name = user.username || user.name || "A"
|
|
40
|
+
if (user.avatar) {
|
|
41
|
+
return `<img class="fw-avatar" src="${user.avatar}" alt="${name}">`
|
|
42
|
+
} else {
|
|
43
|
+
const color = userColor(name)
|
|
44
|
+
return `<span class="fw-string-avatar" style="background-color: ${color};"><span>${name[0]}</span></span>`
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/user_util.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Creates a dropdown box.
|
|
2
|
+
* @param btn The button to open and close the dropdown box.
|
|
3
|
+
* @param box The node containing the contents of the dropdown box.
|
|
4
|
+
* @param preopen An optional function to be called before opening the dropdown box. Used to position dropdown box.
|
|
5
|
+
*/
|
|
6
|
+
export const filterPrimaryEmail = emails => {
|
|
7
|
+
const primaryEmails = emails.filter(email => email.primary)
|
|
8
|
+
if (!primaryEmails.length) {
|
|
9
|
+
if (emails.length) {
|
|
10
|
+
return emails[0].address
|
|
11
|
+
} else {
|
|
12
|
+
return ""
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return primaryEmails[0].address
|
|
16
|
+
}
|
package/src/worker.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* allows cross domain web workers */
|
|
2
|
+
/* Taken from https://benohead.com/cross-domain-cross-browser-web-workers/ */
|
|
3
|
+
export const makeWorker = workerUrl => {
|
|
4
|
+
const a = document.createElement("a")
|
|
5
|
+
a.href = workerUrl // turn into absolute URL if needed.
|
|
6
|
+
const blob = new Blob([`importScripts("${a.href}")`], {
|
|
7
|
+
type: "application/javascript"
|
|
8
|
+
}),
|
|
9
|
+
blobUrl = window.URL.createObjectURL(blob),
|
|
10
|
+
worker = new Worker(blobUrl)
|
|
11
|
+
return worker
|
|
12
|
+
}
|
package/src/ws.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/* Sets up communicating with server (retrieving document, saving, collaboration, etc.).
|
|
2
|
+
*/
|
|
3
|
+
export class WebSocketConnector {
|
|
4
|
+
constructor({
|
|
5
|
+
base = "", // needs to be specified
|
|
6
|
+
path = "", // needs to be specified
|
|
7
|
+
appLoaded = () => false, // required argument
|
|
8
|
+
anythingToSend = () => false, // required argument
|
|
9
|
+
messagesElement = () => false, // element in which to show connection messages
|
|
10
|
+
initialMessage = () => ({type: "subscribe"}),
|
|
11
|
+
resubScribed = () => {}, // Cleanup when the client connects a second or subsequent time
|
|
12
|
+
restartMessage = () => ({type: "restart"}), // Too many messages have been lost and we need to restart
|
|
13
|
+
warningNotAllSent = gettext("Warning! Some data is unsaved"), // Info to show while disconnected WITH unsaved data
|
|
14
|
+
infoDisconnected = gettext("Disconnected. Attempting to reconnect..."), // Info to show while disconnected WITHOUT unsaved data
|
|
15
|
+
receiveData = _data => {},
|
|
16
|
+
failedAuth = () => {
|
|
17
|
+
window.location.href = "/"
|
|
18
|
+
}
|
|
19
|
+
}) {
|
|
20
|
+
this.base = base
|
|
21
|
+
this.path = path
|
|
22
|
+
this.appLoaded = appLoaded
|
|
23
|
+
this.anythingToSend = anythingToSend
|
|
24
|
+
this.messagesElement = messagesElement
|
|
25
|
+
this.initialMessage = initialMessage
|
|
26
|
+
this.resubScribed = resubScribed
|
|
27
|
+
this.restartMessage = restartMessage
|
|
28
|
+
this.warningNotAllSent = warningNotAllSent
|
|
29
|
+
this.infoDisconnected = infoDisconnected
|
|
30
|
+
this.receiveData = receiveData
|
|
31
|
+
this.failedAuth = failedAuth
|
|
32
|
+
/* A list of messages to be sent. Only used when temporarily offline.
|
|
33
|
+
Messages will be sent when returning back online. */
|
|
34
|
+
this.messagesToSend = []
|
|
35
|
+
/* A list of messages from a previous connection */
|
|
36
|
+
this.oldMessages = []
|
|
37
|
+
|
|
38
|
+
this.online = true
|
|
39
|
+
this.connected = false
|
|
40
|
+
/* Increases when connection has to be reestablished */
|
|
41
|
+
/* 0 = before first connection. */
|
|
42
|
+
/* 1 = first connection established, etc. */
|
|
43
|
+
this.connectionCount = 0
|
|
44
|
+
this.recentlySent = false
|
|
45
|
+
this.listeners = {}
|
|
46
|
+
|
|
47
|
+
//heartbeat
|
|
48
|
+
this.pingTimer = false
|
|
49
|
+
this.pongTimer = false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
init() {
|
|
53
|
+
this.createWSConnection()
|
|
54
|
+
|
|
55
|
+
// Close the socket manually for now when the connection is lost. Sometimes the socket isn't closed on disconnection.
|
|
56
|
+
this.listeners.onOffline = _event => this.ws.close()
|
|
57
|
+
window.addEventListener("offline", this.listeners.onOffline)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
goOffline() {
|
|
61
|
+
// Simulate offline mode due to lack of ways of doing this in Chrome/Firefox
|
|
62
|
+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1421357
|
|
63
|
+
// https://bugs.chromium.org/p/chromium/issues/detail?id=423246
|
|
64
|
+
this.online = false
|
|
65
|
+
this.connected = false
|
|
66
|
+
this.ws.close()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
goOnline() {
|
|
70
|
+
// Reconnect from offline mode
|
|
71
|
+
this.online = true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
close() {
|
|
75
|
+
if (this.ws) {
|
|
76
|
+
this.ws.onclose = () => {}
|
|
77
|
+
this.ws.close()
|
|
78
|
+
}
|
|
79
|
+
window.removeEventListener("offline", this.listeners.onOffline)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
createWSConnection() {
|
|
83
|
+
// Messages object used to ensure that data is received in right order.
|
|
84
|
+
this.messages = {
|
|
85
|
+
server: 0,
|
|
86
|
+
client: 0,
|
|
87
|
+
lastTen: []
|
|
88
|
+
}
|
|
89
|
+
let url
|
|
90
|
+
if (this.online) {
|
|
91
|
+
if (this.base.startsWith("/")) {
|
|
92
|
+
url = this.base + this.path
|
|
93
|
+
} else if (location.protocol === "https:") {
|
|
94
|
+
url = `wss://${this.base}${this.path}`
|
|
95
|
+
} else {
|
|
96
|
+
url = `ws://${this.base}${this.path}`
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
if (location.protocol === "https:") {
|
|
100
|
+
url = "wss://offline"
|
|
101
|
+
} else {
|
|
102
|
+
url = "ws://offline"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (this.ws) {
|
|
106
|
+
this.ws.onmessage = () => {}
|
|
107
|
+
this.ws.onclose = () => {}
|
|
108
|
+
this.ws.close()
|
|
109
|
+
}
|
|
110
|
+
this.ws = new window.WebSocket(url)
|
|
111
|
+
this.ws.onmessage = event => this.onmessage(event)
|
|
112
|
+
this.ws.onclose = () => this.onclose()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
waitForWS() {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const checkState = () => {
|
|
118
|
+
if (!this.ws) {
|
|
119
|
+
// WebSocket doesn't exist
|
|
120
|
+
return setTimeout(() => checkState(), 100)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (this.ws.readyState === this.ws.OPEN) {
|
|
124
|
+
// WebSocket is open and ready
|
|
125
|
+
return resolve()
|
|
126
|
+
} else if (this.ws.readyState === this.ws.CONNECTING) {
|
|
127
|
+
// WebSocket is still connecting, wait
|
|
128
|
+
return setTimeout(() => checkState(), 100)
|
|
129
|
+
} else {
|
|
130
|
+
// WebSocket is in CLOSING or CLOSED state
|
|
131
|
+
// We should not try to send on this socket
|
|
132
|
+
return reject(new Error("WebSocket is not in OPEN state"))
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
checkState()
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onmessage(event) {
|
|
141
|
+
const data = JSON.parse(event.data)
|
|
142
|
+
const expectedServer = this.messages.server + 1
|
|
143
|
+
if (data.type === "request_resend") {
|
|
144
|
+
this.resend_messages(data.from)
|
|
145
|
+
} else if (data.type === "pong") {
|
|
146
|
+
this.heartbeat()
|
|
147
|
+
} else if (data.s < expectedServer) {
|
|
148
|
+
// Receive a message already received at least once. Ignore.
|
|
149
|
+
return
|
|
150
|
+
} else if (data.s > expectedServer) {
|
|
151
|
+
// Messages from the server have been lost.
|
|
152
|
+
// Request resend.
|
|
153
|
+
this.waitForWS()
|
|
154
|
+
.then(() =>
|
|
155
|
+
this.ws.send(
|
|
156
|
+
JSON.stringify({
|
|
157
|
+
type: "request_resend",
|
|
158
|
+
from: this.messages.server
|
|
159
|
+
})
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
.catch(() => {
|
|
163
|
+
// Connection not ready, we will handle this on reconnection
|
|
164
|
+
})
|
|
165
|
+
} else {
|
|
166
|
+
this.messages.server = expectedServer
|
|
167
|
+
if (data.c === this.messages.client) {
|
|
168
|
+
this.receive(data)
|
|
169
|
+
} else if (data.c < this.messages.client) {
|
|
170
|
+
// We have received all server messages, but the server seems
|
|
171
|
+
// to have missed some of the client's messages. They could
|
|
172
|
+
// have been sent simultaneously.
|
|
173
|
+
// The server wins over the client in this case.
|
|
174
|
+
this.waitForWS().then(() => {
|
|
175
|
+
const clientDifference = this.messages.client - data.c
|
|
176
|
+
this.messages.client = data.c
|
|
177
|
+
if (clientDifference > this.messages.lastTen.length) {
|
|
178
|
+
// We cannot fix the situation
|
|
179
|
+
this.send(this.restartMessage)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
this.messages["lastTen"]
|
|
183
|
+
.slice(0 - clientDifference)
|
|
184
|
+
.forEach(data => {
|
|
185
|
+
this.messages.client += 1
|
|
186
|
+
data.c = this.messages.client
|
|
187
|
+
data.s = this.messages.server
|
|
188
|
+
|
|
189
|
+
this.ws.send(JSON.stringify(data))
|
|
190
|
+
})
|
|
191
|
+
this.receive(data)
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
onclose() {
|
|
198
|
+
this.connected = false
|
|
199
|
+
window.setTimeout(() => {
|
|
200
|
+
this.createWSConnection()
|
|
201
|
+
}, 2000)
|
|
202
|
+
if (!this.appLoaded()) {
|
|
203
|
+
// doc not initiated
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const messagesElement = this.messagesElement()
|
|
208
|
+
if (messagesElement) {
|
|
209
|
+
if (this.anythingToSend()) {
|
|
210
|
+
messagesElement.innerHTML = `<span class="warn">${this.warningNotAllSent}</span>`
|
|
211
|
+
} else {
|
|
212
|
+
messagesElement.innerHTML = this.infoDisconnected
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
open() {
|
|
218
|
+
const messagesElement = this.messagesElement()
|
|
219
|
+
if (messagesElement) {
|
|
220
|
+
messagesElement.innerHTML = ""
|
|
221
|
+
}
|
|
222
|
+
this.connected = true
|
|
223
|
+
|
|
224
|
+
const message = this.initialMessage()
|
|
225
|
+
this.connectionCount++
|
|
226
|
+
this.oldMessages = this.messagesToSend
|
|
227
|
+
this.messagesToSend = []
|
|
228
|
+
|
|
229
|
+
this.send(() => message)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
subscribed() {
|
|
233
|
+
if (this.connectionCount > 1) {
|
|
234
|
+
this.resubScribed()
|
|
235
|
+
}
|
|
236
|
+
while (this.oldMessages.length > 0) {
|
|
237
|
+
this.send(this.oldMessages.shift())
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Sends data to server or keeps it in a list if currently offline. */
|
|
242
|
+
send(getData, timer = 80) {
|
|
243
|
+
if (this.connected && this.ws.readyState !== this.ws.OPEN) {
|
|
244
|
+
this.ws.close()
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
if (this.connected && !this.recentlySent) {
|
|
248
|
+
const data = getData()
|
|
249
|
+
if (!data) {
|
|
250
|
+
// message is empty
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
this.messages.client += 1
|
|
254
|
+
data.c = this.messages.client
|
|
255
|
+
data.s = this.messages.server
|
|
256
|
+
this.messages.lastTen.push(data)
|
|
257
|
+
this.messages.lastTen = this.messages["lastTen"].slice(-10)
|
|
258
|
+
|
|
259
|
+
this.waitForWS()
|
|
260
|
+
.then(() => {
|
|
261
|
+
this.ws.send(JSON.stringify(data))
|
|
262
|
+
this.setRecentlySentTimer(timer)
|
|
263
|
+
})
|
|
264
|
+
.catch(() => {
|
|
265
|
+
// Failed to send - likely WebSocket is not open
|
|
266
|
+
// Put the message back into the queue
|
|
267
|
+
this.messages.client -= 1
|
|
268
|
+
this.messagesToSend.unshift(getData)
|
|
269
|
+
// Remove from lastTen to avoid duplicates
|
|
270
|
+
this.messages.lastTen = this.messages.lastTen.slice(0, -1)
|
|
271
|
+
})
|
|
272
|
+
} else {
|
|
273
|
+
this.messagesToSend.push(getData)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
setRecentlySentTimer(timer) {
|
|
278
|
+
this.recentlySent = true
|
|
279
|
+
window.setTimeout(() => {
|
|
280
|
+
this.recentlySent = false
|
|
281
|
+
const oldMessages = this.messagesToSend
|
|
282
|
+
this.messagesToSend = []
|
|
283
|
+
while (oldMessages.length > 0) {
|
|
284
|
+
const getData = oldMessages.shift()
|
|
285
|
+
this.send(getData, Math.min(timer * 1.2, 10000))
|
|
286
|
+
}
|
|
287
|
+
}, timer)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
resend_messages(from) {
|
|
291
|
+
return this.waitForWS()
|
|
292
|
+
.then(() => {
|
|
293
|
+
const toSend = this.messages.client - from
|
|
294
|
+
this.messages.client = from
|
|
295
|
+
if (toSend > this.messages.lastTen.length) {
|
|
296
|
+
// Too many messages requested. Abort.
|
|
297
|
+
this.send(this.restartMessage)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
this.messages.lastTen.slice(0 - toSend).forEach(data => {
|
|
301
|
+
this.messages.client += 1
|
|
302
|
+
data.c = this.messages.client
|
|
303
|
+
data.s = this.messages.server
|
|
304
|
+
this.ws.send(JSON.stringify(data))
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
.catch(() => {
|
|
308
|
+
// Could not send messages - WebSocket not ready
|
|
309
|
+
// Will try again when WebSocket is ready
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
receive(data) {
|
|
314
|
+
switch (data.type) {
|
|
315
|
+
case "redirect":
|
|
316
|
+
this.base = data.base
|
|
317
|
+
break
|
|
318
|
+
case "welcome":
|
|
319
|
+
this.open()
|
|
320
|
+
break
|
|
321
|
+
case "subscribed":
|
|
322
|
+
this.subscribed()
|
|
323
|
+
this.heartbeat()
|
|
324
|
+
break
|
|
325
|
+
case "access_denied":
|
|
326
|
+
this.failedAuth()
|
|
327
|
+
break
|
|
328
|
+
default:
|
|
329
|
+
this.receiveData(data)
|
|
330
|
+
break
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
heartbeat() {
|
|
335
|
+
clearTimeout(this.pingTimer)
|
|
336
|
+
clearTimeout(this.pongTimer)
|
|
337
|
+
this.pingTimer = setTimeout(() => {
|
|
338
|
+
// Don't send ping if WebSocket is not open
|
|
339
|
+
if (this.ws.readyState === this.ws.OPEN) {
|
|
340
|
+
this.ws.send('{"type": "ping"}')
|
|
341
|
+
this.pongTimer = setTimeout(() => {
|
|
342
|
+
this.listeners.onOffline()
|
|
343
|
+
}, 10000)
|
|
344
|
+
}
|
|
345
|
+
}, 60000)
|
|
346
|
+
}
|
|
347
|
+
}
|
package/src/file.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import {escapeText} from "./text.js"
|
|
2
|
-
|
|
3
|
-
export const shortFileTitle = (title, path) => {
|
|
4
|
-
if (!path.length || path.endsWith("/")) {
|
|
5
|
-
return escapeText(title || gettext("Untitled"))
|
|
6
|
-
}
|
|
7
|
-
return escapeText(path.split("/").pop())
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const longFilePath = (title, path, prefix = "") => {
|
|
11
|
-
if (!path.length) {
|
|
12
|
-
path = "/"
|
|
13
|
-
}
|
|
14
|
-
if (path.endsWith("/")) {
|
|
15
|
-
path += title.replace(/\//g, "") || gettext("Untitled")
|
|
16
|
-
}
|
|
17
|
-
if (prefix.length) {
|
|
18
|
-
const pathParts = path.split("/")
|
|
19
|
-
const fileName = pathParts.pop()
|
|
20
|
-
pathParts.push(prefix + fileName)
|
|
21
|
-
path = pathParts.join("/")
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return path
|
|
25
|
-
}
|
package/src/text.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export const escapeText = text => {
|
|
2
|
-
if (typeof text !== "string") {
|
|
3
|
-
return String(text)
|
|
4
|
-
}
|
|
5
|
-
return text
|
|
6
|
-
.replace(/&/g, "&")
|
|
7
|
-
.replace(/</g, "<")
|
|
8
|
-
.replace(/>/g, ">")
|
|
9
|
-
.replace(/"/g, """)
|
|
10
|
-
.replace(
|
|
11
|
-
/[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]/g,
|
|
12
|
-
""
|
|
13
|
-
) // invalid in XML chars
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const unescapeText = text =>
|
|
17
|
-
text
|
|
18
|
-
.replace(/</g, "<")
|
|
19
|
-
.replace(/>/g, ">")
|
|
20
|
-
.replace(/"/g, '"')
|
|
21
|
-
.replace(/&/g, "&")
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Turn string literals into single line, removing spaces at start of line
|
|
25
|
-
*/
|
|
26
|
-
export const noSpaceTmp = (strings, ...values) => {
|
|
27
|
-
const tmpStrings = Array.from(strings)
|
|
28
|
-
|
|
29
|
-
let combined = ""
|
|
30
|
-
while (tmpStrings.length > 0 || values.length > 0) {
|
|
31
|
-
if (tmpStrings.length > 0) {
|
|
32
|
-
combined += tmpStrings.shift()
|
|
33
|
-
}
|
|
34
|
-
if (values.length > 0) {
|
|
35
|
-
combined += values.shift()
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let out = ""
|
|
40
|
-
combined.split("\n").forEach(line => {
|
|
41
|
-
out += line.replace(/^\s*/g, "")
|
|
42
|
-
})
|
|
43
|
-
return out
|
|
44
|
-
}
|