firefly-compiler 0.5.39 → 0.5.40

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.
Files changed (129) hide show
  1. package/.hintrc +4 -4
  2. package/.vscode/settings.json +4 -4
  3. package/bin/Release.ff +157 -157
  4. package/bin/firefly.mjs +1 -1
  5. package/compiler/Builder.ff +275 -275
  6. package/compiler/Compiler.ff +234 -234
  7. package/compiler/Dependencies.ff +186 -186
  8. package/compiler/DependencyLock.ff +17 -17
  9. package/compiler/JsEmitter.ff +1437 -1437
  10. package/compiler/LspHook.ff +202 -202
  11. package/compiler/ModuleCache.ff +178 -178
  12. package/compiler/Workspace.ff +88 -88
  13. package/core/.firefly/include/package.json +5 -5
  14. package/core/.firefly/package.ff +2 -2
  15. package/core/Any.ff +25 -25
  16. package/core/Array.ff +298 -298
  17. package/core/Atomic.ff +63 -63
  18. package/core/Box.ff +7 -7
  19. package/core/BrowserSystem.ff +40 -40
  20. package/core/BuildSystem.ff +156 -156
  21. package/core/Crypto.ff +94 -94
  22. package/core/Equal.ff +41 -41
  23. package/core/Error.ff +25 -25
  24. package/core/HttpClient.ff +142 -142
  25. package/core/Instant.ff +24 -24
  26. package/core/Js.ff +305 -305
  27. package/core/JsSystem.ff +135 -135
  28. package/core/Json.ff +423 -423
  29. package/core/List.ff +482 -482
  30. package/core/Lock.ff +108 -108
  31. package/core/NodeSystem.ff +198 -198
  32. package/core/Ordering.ff +160 -160
  33. package/core/Path.ff +377 -378
  34. package/core/Queue.ff +90 -90
  35. package/core/Random.ff +140 -140
  36. package/core/RbMap.ff +216 -216
  37. package/core/Show.ff +44 -44
  38. package/core/SourceLocation.ff +68 -68
  39. package/core/Task.ff +165 -165
  40. package/experimental/benchmarks/ListGrab.ff +23 -23
  41. package/experimental/benchmarks/ListGrab.java +55 -55
  42. package/experimental/benchmarks/Pyrotek45.ff +30 -30
  43. package/experimental/benchmarks/Pyrotek45.java +64 -64
  44. package/experimental/bidirectional/Bidi.ff +88 -88
  45. package/experimental/lines/Main.ff +40 -40
  46. package/experimental/random/Index.ff +53 -53
  47. package/experimental/random/Process.ff +120 -120
  48. package/experimental/random/RunLength.ff +65 -65
  49. package/experimental/random/Scrape.ff +51 -51
  50. package/experimental/random/Symbols.ff +73 -73
  51. package/experimental/random/Tensor.ff +52 -52
  52. package/experimental/random/Units.ff +36 -36
  53. package/experimental/s3/S3TestAuthorizationHeader.ff +39 -39
  54. package/experimental/s3/S3TestPut.ff +16 -16
  55. package/experimental/tests/TestJson.ff +26 -26
  56. package/firefly.sh +0 -0
  57. package/fireflysite/.firefly/package.ff +4 -4
  58. package/fireflysite/CommunityOverview.ff +20 -20
  59. package/fireflysite/CountingButtonDemo.ff +58 -58
  60. package/fireflysite/DocumentParser.ff +325 -325
  61. package/fireflysite/ExamplesOverview.ff +40 -40
  62. package/fireflysite/FrontPage.ff +344 -344
  63. package/fireflysite/GettingStarted.ff +45 -45
  64. package/fireflysite/Guide.ff +456 -456
  65. package/fireflysite/Main.ff +163 -163
  66. package/fireflysite/MatchingPasswordsDemo.ff +82 -82
  67. package/fireflysite/PackagesOverview.ff +49 -49
  68. package/fireflysite/PostgresqlDemo.ff +34 -34
  69. package/fireflysite/ReferenceAll.ff +18 -18
  70. package/fireflysite/ReferenceIntroduction.ff +11 -11
  71. package/fireflysite/Styles.ff +567 -567
  72. package/fireflysite/Test.ff +121 -121
  73. package/fireflysite/assets/markdown/reference/BaseTypes.md +209 -209
  74. package/fireflysite/assets/markdown/reference/EmittedJavascript.md +65 -65
  75. package/fireflysite/assets/markdown/reference/Exceptions.md +101 -101
  76. package/fireflysite/assets/markdown/reference/FunctionsAndMethods.md +364 -364
  77. package/fireflysite/assets/markdown/reference/JavascriptInterop.md +235 -235
  78. package/fireflysite/assets/markdown/reference/ModulesAndPackages.md +162 -162
  79. package/fireflysite/assets/markdown/reference/OldStructuredConcurrency.md +48 -48
  80. package/fireflysite/assets/markdown/reference/PatternMatching.md +224 -224
  81. package/fireflysite/assets/markdown/reference/StatementsAndExpressions.md +86 -86
  82. package/fireflysite/assets/markdown/reference/StructuredConcurrency.md +99 -99
  83. package/fireflysite/assets/markdown/reference/TraitsAndInstances.md +100 -100
  84. package/fireflysite/assets/markdown/reference/UserDefinedTypes.md +184 -184
  85. package/fireflysite/assets/markdown/scratch/ControlFlow.md +136 -136
  86. package/fireflysite/assets/markdown/scratch/Toc.md +40 -40
  87. package/lsp/.firefly/package.ff +1 -1
  88. package/lsp/CompletionHandler.ff +827 -827
  89. package/lsp/Handler.ff +714 -714
  90. package/lsp/HoverHandler.ff +79 -79
  91. package/lsp/LanguageServer.ff +272 -272
  92. package/lsp/SignatureHelpHandler.ff +55 -55
  93. package/lsp/SymbolHandler.ff +181 -181
  94. package/lsp/TestReferences.ff +17 -17
  95. package/lsp/TestReferencesCase.ff +7 -7
  96. package/lsp/stderr.txt +1 -1
  97. package/lsp/stdout.txt +34 -34
  98. package/lux/.firefly/package.ff +1 -1
  99. package/lux/Css.ff +648 -648
  100. package/lux/CssTest.ff +48 -48
  101. package/lux/Lux.ff +608 -608
  102. package/lux/LuxEvent.ff +79 -79
  103. package/lux/Main.ff +123 -123
  104. package/lux/Main2.ff +143 -143
  105. package/lux/TestDry.ff +28 -28
  106. package/output/js/ff/compiler/Builder.mjs +36 -36
  107. package/output/js/ff/core/Path.mjs +0 -2
  108. package/package.json +1 -1
  109. package/rpc/.firefly/package.ff +1 -1
  110. package/rpc/Rpc.ff +70 -70
  111. package/s3/.firefly/package.ff +1 -1
  112. package/s3/S3.ff +92 -92
  113. package/vscode/LICENSE.txt +21 -21
  114. package/vscode/Prepublish.ff +15 -15
  115. package/vscode/README.md +16 -16
  116. package/vscode/client/package-lock.json +544 -544
  117. package/vscode/client/package.json +22 -22
  118. package/vscode/client/src/extension.ts +104 -104
  119. package/vscode/icons/firefly-icon.svg +10 -10
  120. package/vscode/language-configuration.json +61 -61
  121. package/vscode/package-lock.json +3623 -3623
  122. package/vscode/package.json +1 -1
  123. package/vscode/snippets.json +241 -241
  124. package/vscode/syntaxes/firefly-markdown-injection.json +45 -45
  125. package/webserver/.firefly/include/package.json +5 -5
  126. package/webserver/.firefly/package.ff +2 -2
  127. package/webserver/WebServer.ff +647 -647
  128. package/websocket/.firefly/package.ff +1 -1
  129. package/websocket/WebSocket.ff +100 -100
package/lux/Lux.ff CHANGED
@@ -1,608 +1,608 @@
1
- import Lux
2
- import LuxEvent
3
- import Css
4
-
5
- capability Lux(
6
- document: LuxDocument
7
- jsSystem: JsSystem
8
- mutable dry: Option[Array[DryNode]]
9
- mutable cssClasses: StringMap[CssClass]
10
- mutable renderLock: Lock
11
- mutable task: Task
12
- mutable renderQueue: Array[RenderQueueItem]
13
- mutable element: LuxElement
14
- mutable depth: Int
15
- mutable keys: Option[StringMap[JsValue]]
16
- mutable key: String
17
- mutable attributes: Option[StringMap[String]]
18
- mutable texts: Array[String]
19
- )
20
-
21
- capability RenderQueueItem(
22
- luxCopy: Lux
23
- render: () => Unit
24
- )
25
-
26
- class LuxElement(element: JsValue, mutable child: Int, mutable keepChildren: Bool)
27
-
28
- class LuxDocument(document: JsValue)
29
-
30
- data LuxInvaldNameException(name: String)
31
-
32
- class DryNode {
33
- DryElement(tagName: String, attributes: StringMap[String], children: Array[DryNode])
34
- DryFragment(children: Array[DryNode])
35
- DryText(text: String)
36
- }
37
-
38
- extend self: DryNode {
39
- toHtml(): String {
40
- // https://www.w3.org/TR/xml/ - wrt. checkXmmlName, we only accept the ASCII subset
41
- function checkXmlName(name: String): String {
42
- if(name.size() == 0) {throw(LuxInvaldNameException(name))}
43
- if(!name.grabFirst().isAsciiLetter() && !name.startsWith(":") && !name.startsWith("_")) {throw(LuxInvaldNameException(name))}
44
- if(!name.all {c => c.isAsciiLetterOrDigit() || c == ':' || c == '_' || c == '-' || c == '.'}) {throw(LuxInvaldNameException(name))}
45
- name
46
- }
47
- function escapeAttributeValue(value: String): String {
48
- value.replace("&", "&#x26;").replace("<", "&#x3C;").replace(">", "&#x3E;").replace("\"", "&#x22;")
49
- }
50
- function escapeText(text: String): String {
51
- text.replace("&", "&#x26;").replace("<", "&#x3C;").replace(">", "&#x3E;")
52
- }
53
- let voidHtmlElements = [
54
- "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
55
- ].toSet()
56
- function toHtml(node: DryNode): String {
57
- | DryElement(tagName, attributes, children) =>
58
- let attributeHtml = attributes.toList().map {| Pair(name, value) =>
59
- " " + checkXmlName(name) + "=\"" + escapeAttributeValue(value) + "\""
60
- }.join()
61
- let childrenHtml = children.toList().map {toHtml(_)}.join()
62
- let endHtml = if(voidHtmlElements.contains(tagName)) {""} else {"</" + tagName + ">"}
63
- "<" + checkXmlName(tagName) + attributeHtml + ">" + childrenHtml + endHtml
64
- | DryFragment(children) =>
65
- children.toList().map {toHtml(_)}.join()
66
- | DryText(text) =>
67
- escapeText(text)
68
- }
69
- toHtml(self)
70
- }
71
- }
72
-
73
- extend self: LuxElement {
74
- childAt(index: Int): JsValue {
75
- self.element->childNodes.get(index)
76
- }
77
- insertBefore(newNode: JsValue, referenceNode: JsValue): JsValue {
78
- self.element->insertBefore(newNode, referenceNode)
79
- }
80
- removeAt(index: Int): Bool {
81
- let node = self.childAt(index)
82
- let remove = !node.isNullOrUndefined()
83
- if(remove) {
84
- self.element->removeChild(node)
85
- }
86
- remove
87
- }
88
- }
89
-
90
- extend self: LuxDocument {
91
- createTextNode(value: String): JsValue {
92
- self.document->createTextNode(value)
93
- }
94
-
95
- createElement(tagName: String): JsValue {
96
- self.document->createElement(tagName)
97
- }
98
-
99
- createFragment(): JsValue {
100
- self.document->createDocumentFragment()
101
- }
102
- }
103
-
104
- extend self: Lux {
105
-
106
- copyFrom(lux: Lux) {
107
- self.renderLock = lux.renderLock
108
- self.renderQueue = lux.renderQueue
109
- self.element = lux.element.LuxElement()
110
- self.depth = lux.depth
111
- self.keys = lux.keys
112
- self.key = lux.key
113
- self.attributes = lux.attributes
114
- }
115
-
116
- text(value: String) {
117
- if(value.size() != 0) {
118
- self.texts.push(value)
119
- }
120
- }
121
-
122
- add(tagName: String, body: () => Unit = {}) {
123
- patchText(self)
124
- self.dry.map {dry =>
125
- self.dry = Some([].toArray())
126
- let savedAttributes = self.attributes
127
- let savedKeys = self.keys
128
- self.attributes = if(self.key != "") {
129
- let attributes = StringMap.new()
130
- attributes.set("data-lux-key", tagName.upper() + ">" + self.key)
131
- attributes
132
- }
133
- self.key = ""
134
- self.depth += 1
135
- try {
136
- body()
137
- } finally {
138
- patchText(self)
139
- dry.push(DryElement(tagName, self.attributes.else {StringMap.new()}, self.dry.grab()))
140
- self.depth -= 1
141
- self.attributes = savedAttributes
142
- self.keys = savedKeys
143
- self.element.child += 1
144
- self.dry = Some(dry)
145
- }
146
- }.else:
147
- let node = patchElement(self, tagName)
148
- if(!node->luxHandlers.isNullOrUndefined()) {
149
- node->luxHandlers.grabArray().each {pair =>
150
- node->removeEventListener(pair->event, pair->handler)
151
- }
152
- node->luxHandlers = self.jsSystem.array([])
153
- }
154
- let savedAttributes = self.attributes
155
- let savedKeys = self.keys
156
- let savedElement = self.element
157
- self.attributes = if(self.key != "") {
158
- let attributes = StringMap.new()
159
- attributes.set("data-lux-key", tagName.upper() + ">" + self.key)
160
- attributes
161
- }
162
- self.element = LuxElement(node, 0, keepChildren = False)
163
- //self.element.element->classList->value = ""
164
- self.keys = None
165
- self.key = ""
166
- self.depth += 1
167
- try {
168
- body()
169
- } finally {
170
- patchText(self)
171
- if(!self.element.keepChildren) {
172
- doWhile {removeCurrentChild(self)}
173
- }
174
- patchAttributes(self)
175
- self.depth -= 1
176
- self.attributes = savedAttributes
177
- self.keys = savedKeys
178
- self.element = savedElement
179
- self.element.child += 1
180
- }
181
- }
182
-
183
- div(body: () => Unit = {}) {self.add("div", body)}
184
- span(body: () => Unit = {}) {self.add("span", body)}
185
- label(body: () => Unit = {}) {self.add("label", body)}
186
- button(body: () => Unit = {}) {self.add("button", body)}
187
- form(body: () => Unit = {}) {self.add("form", body)}
188
- input(body: () => Unit = {}) {self.add("input", body)}
189
-
190
- keyed(key: String, body: () => Unit) {
191
- try {
192
- self.key = key
193
- body()
194
- } finally {
195
- self.key = ""
196
- }
197
- }
198
-
199
- keyedList[T](values: List[T], getKey: T => String, body: T => Unit) {
200
- try {
201
- values.each {item =>
202
- self.key = getKey(item)
203
- body(item)
204
- }
205
- } finally {
206
- self.key = ""
207
- }
208
- }
209
-
210
- set(attribute: String, value: String) {
211
- let attributes = self.attributes.else {
212
- let map = StringMap.new()
213
- self.attributes = Some(map)
214
- map
215
- }
216
- attributes.set(attribute, value)
217
- }
218
-
219
- setId(value: String) {self.set("id", value)}
220
-
221
- setValue(value: String) { // TODO: Not an attribute
222
- self.dry.map {_ => self.set("value", value)}.else:
223
- self.element.element->value = value
224
- }
225
-
226
- css(style: Css) {
227
- let attributes = self.attributes.else {
228
- let map = StringMap.new()
229
- self.attributes = Some(map)
230
- map
231
- }
232
- if(attributes.has("style")) {
233
- attributes.set("style", attributes.grab("style") + ";" + style.property + ":" + style.value)
234
- } else {
235
- attributes.set("style", style.property + ":" + style.value)
236
- }
237
- }
238
-
239
- cssClass(class: CssClass) {
240
- if(!self.cssClasses.has(class.name())) {
241
- self.cssClasses.set(class.name(), class)
242
- if(self.dry.isEmpty()):
243
- let styleSheet = self.document.createElement("style")
244
- styleSheet->textContent = class.show()
245
- self.document.document->head->appendChild(styleSheet)
246
- }
247
- let classNames = self.attributes.flatMap {_.get("class")}.map {_ + " "}.else {""}
248
- self.set("class", classNames + class.name())
249
- }
250
-
251
- on(event: String, handler: LuxEvent => Unit) {
252
- if(self.dry.isEmpty()):
253
- let jsHandler = Js->{jsEvent =>
254
- self.renderLock.do {
255
- handler(LuxEvent(jsEvent))
256
- processRenderQueue(self)!
257
- }
258
- }
259
- self.element.element->addEventListener(event, jsHandler)
260
- if(self.element.element->luxHandlers.isNullOrUndefined()) {
261
- self.element.element->luxHandlers = self.jsSystem.array([])
262
- }
263
- self.element.element->luxHandlers->push(
264
- self.jsSystem.object().with("event", event).with("handler", jsHandler)
265
- )
266
- }
267
-
268
- onClick(handler: LuxEvent => Unit) {self.on("click", handler)}
269
- onInput(handler: LuxEvent => Unit) {self.on("input", handler)}
270
-
271
- useState[T: HasAnyTag](initialValue: T, body: (T, T => Unit) => Unit) {
272
- self.dry.map {_ => body(initialValue, {_ => })}.else:
273
- self.depth += 1
274
- let value = getStateOnElement(self.element.element, self.depth, initialValue)
275
- mutable i = 0
276
- while {i < self.renderQueue.size()} {
277
- let item = self.renderQueue.grab(i)
278
- if(item.luxCopy.element.element.equals(self.element.element) && item.luxCopy.depth == self.depth) {
279
- self.renderQueue.delete(i, 1)
280
- } else {
281
- i += 1
282
- }
283
- }
284
- let luxCopy = self.Lux(element = self.element.LuxElement())
285
- function setState(newValue: T): Unit {
286
- setStateOnElement(luxCopy.element.element, luxCopy.depth, newValue)
287
- self.renderQueue.push(RenderQueueItem(
288
- luxCopy = luxCopy
289
- render = {
290
- let child = luxCopy.element.child
291
- let element = luxCopy.element.childAt(child)
292
- body(newValue, {setState(_)})
293
- if(!element.equals(luxCopy.element.childAt(child))) {
294
- removeCurrentChild(luxCopy.Lux(
295
- element = luxCopy.element.LuxElement(child = luxCopy.element.child + 1)
296
- ))
297
- }
298
- }
299
- ))
300
- }
301
- try {
302
- body(value, {setState(_)})
303
- } finally {
304
- self.depth -= 1
305
- }
306
- }
307
-
308
- useCallback1[A1: Equal: HasAnyTag](callback: A1 => Unit, body: (A1 => Unit) => Unit) {
309
- self.dry.map {_ => body(callback)}.else:
310
- self.depth += 1
311
- setStateOnElement(self.element.element, self.depth, callback)
312
- let element = self.element.element
313
- let depth = self.depth
314
- try {
315
- body {a1 =>
316
- let latestCallback = getStateOnElement(element, depth, callback)
317
- latestCallback(a1)
318
- }
319
- } finally {
320
- self.depth -= 1
321
- }
322
- }
323
-
324
- useLazy1[A1: Equal: HasAnyTag](a1: A1, body: A1 => Unit = {_ => }) {
325
- self.dry.map {_ => body(a1)}.else:
326
- self.depth += 1
327
- try {
328
- let old = getStateOnElement(self.element.element, self.depth, None)
329
- old.{
330
- | Some(o1) {a1 == o1} =>
331
- self.element.keepChildren = True
332
- | _ =>
333
- setStateOnElement(self.element.element, self.depth, Some(a1))
334
- body(a1)
335
- }
336
- } finally {
337
- self.depth -= 1
338
- }
339
- }
340
-
341
- useMemo1[A1: Equal: HasAnyTag, T: HasAnyTag](a1: A1, compute: A1 => T, body: T => Unit) {
342
- self.dry.map {_ => body(compute(a1))}.else:
343
- self.depth += 1
344
- try {
345
- let old = getStateOnElement(self.element.element, self.depth, Pair(a1, None))
346
- let computed = old.{
347
- | Pair(o1, _) {a1 != o1} =>
348
- let v = compute(a1)
349
- setStateOnElement(self.element.element, self.depth, Pair(a1, Some(v)))
350
- v
351
- | Pair(_, Some(v)) =>
352
- v
353
- | Pair(_, None) =>
354
- let v = compute(a1)
355
- setStateOnElement(self.element.element, self.depth, Pair(a1, Some(v)))
356
- v
357
- }
358
- body(computed)
359
- } finally {
360
- self.depth -= 1
361
- }
362
- }
363
-
364
- useSuspense(suspense: () => Unit, body: Lux => Unit) {
365
- self.dry.map {_ => suspense()}.else:
366
- let oldSubtask = getTaskOnElement(self.element.element)
367
- oldSubtask.each {task =>
368
- forceAsyncAbort(self, task)
369
- }
370
- let fragment = self.document.createFragment()
371
- let luxCopy = self.Lux(element = self.element.LuxElement())
372
- let subtask = self.task.spawn {task =>
373
- let lux = luxCopy.Lux(
374
- task = task
375
- renderLock = task.lock()
376
- renderQueue = Array.new()
377
- element = luxCopy.element.LuxElement(element = fragment)
378
- )
379
- try {
380
- body(lux)
381
- task.throwIfAborted()
382
- lux.copyFrom(luxCopy)
383
- lux.renderLock.do {
384
- abortTasksOnElement(lux, lux.element.childAt(lux.element.child), onlyChildren = True)
385
- lux.element.removeAt(lux.element.child)
386
- lux.element.insertBefore(fragment, lux.jsSystem.null())
387
- }
388
- } catchAny {error =>
389
- if(error.name() != "AbortError") {
390
- error.rethrow()
391
- }
392
- }
393
- }
394
- suspense()
395
- setTaskOnElement(self.element.childAt(self.element.child - 1), Some(subtask))
396
- }
397
-
398
- }
399
-
400
- processRenderQueue(self: Lux) {
401
- self.renderQueue.sortBy {-_.luxCopy.depth}
402
- let newLux = self.Lux(element = self.element.LuxElement())
403
- try {
404
- while {!self.renderQueue.isEmpty()} {
405
- let item = self.renderQueue.pop().grab()
406
- self.copyFrom(item.luxCopy)
407
- item.render()
408
- }
409
- } finally {
410
- self.copyFrom(newLux)
411
- }
412
- }
413
-
414
- removeCurrentChild(self: Lux): Bool {
415
- let child = self.element.childAt(self.element.child)
416
- if(!child.isNullOrUndefined() && !child->children.isNullOrUndefined()) {
417
- abortTasksOnElement(self, child, False)
418
- }
419
- self.element.removeAt(self.element.child)
420
- }
421
-
422
- getStateOnElement[T /*: HasAnyTag*/](element: JsValue, depth: Int, fallback: T): T {
423
- let value = element.get("lux" + depth)
424
- if(value.isNullOrUndefined()) {fallback} else {
425
- value?
426
- }
427
- }
428
-
429
- setStateOnElement[T /*: HasAnyTag*/](element: JsValue, depth: Int, value: T): Unit {
430
- element.set("lux" + depth, value!)
431
- }
432
-
433
-
434
- getTaskOnElement(element: JsValue): Option[Task] {
435
- let value = element->luxTask
436
- if(value.isNullOrUndefined()) {None} else {
437
- value?
438
- }
439
- }
440
-
441
- setTaskOnElement(element: JsValue, task: Option[Task]): Unit {
442
- element->luxTask = task!
443
- }
444
-
445
- abortTasksOnElement(lux: Lux, element: JsValue, onlyChildren: Bool): Unit {
446
- if(!onlyChildren) {
447
- getTaskOnElement(element).each {task => forceAsyncAbort(lux, task)}
448
- }
449
- element->children.each {child =>
450
- abortTasksOnElement(lux, child, False)
451
- }
452
- }
453
-
454
- forceAsyncAbort(self: Lux, task: Task): Unit {
455
- task.abort()
456
- }
457
-
458
- patchText(self: Lux) {
459
- if(!self.texts.isEmpty()):
460
- let value = self.texts.drain().join()
461
- self.dry.map {_.push(DryText(value))}.else:
462
- let oldNode = self.element.childAt(self.element.child)
463
- let oldValue = if(!oldNode.isNullOrUndefined()) {oldNode->data} else {oldNode}
464
- if(oldValue.isNullOrUndefined() || oldValue.grabString() != value) {
465
- let node = self.document.createTextNode(value)
466
- self.element.insertBefore(node, oldNode)
467
- }
468
- self.element.child += 1
469
- }
470
-
471
- patchElement(self: Lux, tagName: String): JsValue {
472
- let newKey = if(self.key != "") {tagName.upper() + ">" + self.key} else {""}
473
- let oldNode = self.element.childAt(self.element.child)
474
- let oldKey = if(!oldNode.isNullOrUndefined() && !oldNode->getAttribute.isNullOrUndefined()) {
475
- oldNode->getAttribute("data-lux-key")
476
- } else {oldNode}
477
- let match = if(newKey != "") {
478
- !oldKey.isNullOrUndefined() &&
479
- oldKey.grabString() == newKey
480
- } else {
481
- oldKey.isNullOrUndefined() &&
482
- !oldNode.isNullOrUndefined() &&
483
- !oldNode->tagName.isNullOrUndefined() &&
484
- oldNode->tagName.grabString() == tagName.upper()
485
- }
486
- if(match) {
487
- oldNode
488
- } else {
489
- let newNode = if(newKey == "") {self.document.createElement(tagName)} else {
490
- let keys = self.keys.else {
491
- let map = StringMap.new()
492
- mutable i = self.element.child
493
- mutable c = self.element.childAt(i)
494
- while {!c.isNullOrUndefined()} {
495
- if(!c->getAttribute.isNullOrUndefined()) {
496
- let k = c->getAttribute("data-lux-key")
497
- if(!k.isNullOrUndefined()) {map.set(k.grabString(), c)}
498
- }
499
- i += 1
500
- c = self.element.childAt(i)
501
- }
502
- self.keys = Some(map)
503
- map
504
- }
505
- if(keys.has(newKey)) {
506
- let foundNode = keys.grab(newKey)
507
- keys.remove(newKey)
508
- foundNode
509
- } else {
510
- let createdNode = self.document.createElement(tagName)
511
- createdNode->setAttribute("data-lux-key", newKey)
512
- createdNode
513
- }
514
- }
515
- self.element.insertBefore(newNode, oldNode)
516
- newNode
517
- }
518
- }
519
-
520
- patchAttributes(self: Lux) {
521
- let attributes = self.element.element->attributes
522
- self.attributes.{
523
- | None =>
524
- mutable i = attributes->length.grabInt() - 1
525
- while {i >= 0} {
526
- let attribute = attributes.get(i)
527
- self.element.element->removeAttribute(attribute->name)
528
- i -= 1
529
- }
530
- | Some(map) =>
531
- mutable i = attributes->length.grabInt() - 1
532
- while {i >= 0} {
533
- let attribute = attributes.get(i)
534
- if(!map.has(attribute->name.grabString())) {
535
- self.element.element->removeAttribute(attribute->name)
536
- }
537
- i -= 1
538
- }
539
- map.each {name, value =>
540
- let oldValue = self.element.element->getAttribute(name)
541
- if(oldValue.isNullOrUndefined() || !oldValue.equals(value)) {
542
- self.element.element->setAttribute(name, value)
543
- }
544
- }
545
- }
546
- }
547
-
548
- render(browserSystem: BrowserSystem, element: JsValue, body: Lux => Unit) {
549
- mutable document = element
550
- while {!document->parentNode.isNullOrUndefined()} {
551
- document = document->parentNode
552
- }
553
- // [...document.querySelectorAll("[lux-class]")].map(x => x.getAttribute("lux-class"))
554
- let staticCssClasses = StringMap.new()
555
- let dummyCssClass = CssClass([], [], [])
556
- document->querySelectorAll("[lux-class]").each {e =>
557
- staticCssClasses.set(e->getAttribute("lux-class").grabString(), dummyCssClass)
558
- }
559
- let lux = Lux(
560
- jsSystem = browserSystem.js()
561
- renderLock = browserSystem.mainTask().lock()
562
- dry = None
563
- cssClasses = staticCssClasses
564
- task = browserSystem.mainTask()
565
- depth = 0
566
- document = LuxDocument(document)
567
- element = LuxElement(element, 0, keepChildren = False)
568
- keys = None
569
- key = ""
570
- attributes = None
571
- texts = Array.new()
572
- renderQueue = Array.new()
573
- )
574
- lux.renderLock.do {
575
- body(lux)
576
- patchText(lux)
577
- }
578
- }
579
-
580
- renderById(browserSystem: BrowserSystem, id: String, body: Lux => Unit) {
581
- let element = browserSystem.js()->document->getElementById(id)
582
- render(browserSystem, element, body)
583
- }
584
-
585
- renderToString(nodeSystem: NodeSystem, body: Lux => Unit): Pair[String, String] {
586
- let children = [].toArray()
587
- let lux = Lux(
588
- jsSystem = nodeSystem.js()
589
- renderLock = nodeSystem.mainTask().lock()
590
- dry = Some(children)
591
- cssClasses = StringMap.new()
592
- task = nodeSystem.mainTask()
593
- depth = 0
594
- document = LuxDocument(nodeSystem.js().null())
595
- element = LuxElement(nodeSystem.js().null(), 0, keepChildren = False)
596
- keys = None
597
- key = ""
598
- attributes = None
599
- texts = Array.new()
600
- renderQueue = Array.new()
601
- )
602
- body(lux)
603
- patchText(lux)
604
- let styleTags = lux.cssClasses.values().map {c =>
605
- "<style lux-class=\"" + c.name() + "\">" + c.show() + "</style>"
606
- }.join()
607
- Pair(children.toList().map {_.toHtml()}.join(), styleTags)
608
- }
1
+ import Lux
2
+ import LuxEvent
3
+ import Css
4
+
5
+ capability Lux(
6
+ document: LuxDocument
7
+ jsSystem: JsSystem
8
+ mutable dry: Option[Array[DryNode]]
9
+ mutable cssClasses: StringMap[CssClass]
10
+ mutable renderLock: Lock
11
+ mutable task: Task
12
+ mutable renderQueue: Array[RenderQueueItem]
13
+ mutable element: LuxElement
14
+ mutable depth: Int
15
+ mutable keys: Option[StringMap[JsValue]]
16
+ mutable key: String
17
+ mutable attributes: Option[StringMap[String]]
18
+ mutable texts: Array[String]
19
+ )
20
+
21
+ capability RenderQueueItem(
22
+ luxCopy: Lux
23
+ render: () => Unit
24
+ )
25
+
26
+ class LuxElement(element: JsValue, mutable child: Int, mutable keepChildren: Bool)
27
+
28
+ class LuxDocument(document: JsValue)
29
+
30
+ data LuxInvaldNameException(name: String)
31
+
32
+ class DryNode {
33
+ DryElement(tagName: String, attributes: StringMap[String], children: Array[DryNode])
34
+ DryFragment(children: Array[DryNode])
35
+ DryText(text: String)
36
+ }
37
+
38
+ extend self: DryNode {
39
+ toHtml(): String {
40
+ // https://www.w3.org/TR/xml/ - wrt. checkXmmlName, we only accept the ASCII subset
41
+ function checkXmlName(name: String): String {
42
+ if(name.size() == 0) {throw(LuxInvaldNameException(name))}
43
+ if(!name.grabFirst().isAsciiLetter() && !name.startsWith(":") && !name.startsWith("_")) {throw(LuxInvaldNameException(name))}
44
+ if(!name.all {c => c.isAsciiLetterOrDigit() || c == ':' || c == '_' || c == '-' || c == '.'}) {throw(LuxInvaldNameException(name))}
45
+ name
46
+ }
47
+ function escapeAttributeValue(value: String): String {
48
+ value.replace("&", "&#x26;").replace("<", "&#x3C;").replace(">", "&#x3E;").replace("\"", "&#x22;")
49
+ }
50
+ function escapeText(text: String): String {
51
+ text.replace("&", "&#x26;").replace("<", "&#x3C;").replace(">", "&#x3E;")
52
+ }
53
+ let voidHtmlElements = [
54
+ "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
55
+ ].toSet()
56
+ function toHtml(node: DryNode): String {
57
+ | DryElement(tagName, attributes, children) =>
58
+ let attributeHtml = attributes.toList().map {| Pair(name, value) =>
59
+ " " + checkXmlName(name) + "=\"" + escapeAttributeValue(value) + "\""
60
+ }.join()
61
+ let childrenHtml = children.toList().map {toHtml(_)}.join()
62
+ let endHtml = if(voidHtmlElements.contains(tagName)) {""} else {"</" + tagName + ">"}
63
+ "<" + checkXmlName(tagName) + attributeHtml + ">" + childrenHtml + endHtml
64
+ | DryFragment(children) =>
65
+ children.toList().map {toHtml(_)}.join()
66
+ | DryText(text) =>
67
+ escapeText(text)
68
+ }
69
+ toHtml(self)
70
+ }
71
+ }
72
+
73
+ extend self: LuxElement {
74
+ childAt(index: Int): JsValue {
75
+ self.element->childNodes.get(index)
76
+ }
77
+ insertBefore(newNode: JsValue, referenceNode: JsValue): JsValue {
78
+ self.element->insertBefore(newNode, referenceNode)
79
+ }
80
+ removeAt(index: Int): Bool {
81
+ let node = self.childAt(index)
82
+ let remove = !node.isNullOrUndefined()
83
+ if(remove) {
84
+ self.element->removeChild(node)
85
+ }
86
+ remove
87
+ }
88
+ }
89
+
90
+ extend self: LuxDocument {
91
+ createTextNode(value: String): JsValue {
92
+ self.document->createTextNode(value)
93
+ }
94
+
95
+ createElement(tagName: String): JsValue {
96
+ self.document->createElement(tagName)
97
+ }
98
+
99
+ createFragment(): JsValue {
100
+ self.document->createDocumentFragment()
101
+ }
102
+ }
103
+
104
+ extend self: Lux {
105
+
106
+ copyFrom(lux: Lux) {
107
+ self.renderLock = lux.renderLock
108
+ self.renderQueue = lux.renderQueue
109
+ self.element = lux.element.LuxElement()
110
+ self.depth = lux.depth
111
+ self.keys = lux.keys
112
+ self.key = lux.key
113
+ self.attributes = lux.attributes
114
+ }
115
+
116
+ text(value: String) {
117
+ if(value.size() != 0) {
118
+ self.texts.push(value)
119
+ }
120
+ }
121
+
122
+ add(tagName: String, body: () => Unit = {}) {
123
+ patchText(self)
124
+ self.dry.map {dry =>
125
+ self.dry = Some([].toArray())
126
+ let savedAttributes = self.attributes
127
+ let savedKeys = self.keys
128
+ self.attributes = if(self.key != "") {
129
+ let attributes = StringMap.new()
130
+ attributes.set("data-lux-key", tagName.upper() + ">" + self.key)
131
+ attributes
132
+ }
133
+ self.key = ""
134
+ self.depth += 1
135
+ try {
136
+ body()
137
+ } finally {
138
+ patchText(self)
139
+ dry.push(DryElement(tagName, self.attributes.else {StringMap.new()}, self.dry.grab()))
140
+ self.depth -= 1
141
+ self.attributes = savedAttributes
142
+ self.keys = savedKeys
143
+ self.element.child += 1
144
+ self.dry = Some(dry)
145
+ }
146
+ }.else:
147
+ let node = patchElement(self, tagName)
148
+ if(!node->luxHandlers.isNullOrUndefined()) {
149
+ node->luxHandlers.grabArray().each {pair =>
150
+ node->removeEventListener(pair->event, pair->handler)
151
+ }
152
+ node->luxHandlers = self.jsSystem.array([])
153
+ }
154
+ let savedAttributes = self.attributes
155
+ let savedKeys = self.keys
156
+ let savedElement = self.element
157
+ self.attributes = if(self.key != "") {
158
+ let attributes = StringMap.new()
159
+ attributes.set("data-lux-key", tagName.upper() + ">" + self.key)
160
+ attributes
161
+ }
162
+ self.element = LuxElement(node, 0, keepChildren = False)
163
+ //self.element.element->classList->value = ""
164
+ self.keys = None
165
+ self.key = ""
166
+ self.depth += 1
167
+ try {
168
+ body()
169
+ } finally {
170
+ patchText(self)
171
+ if(!self.element.keepChildren) {
172
+ doWhile {removeCurrentChild(self)}
173
+ }
174
+ patchAttributes(self)
175
+ self.depth -= 1
176
+ self.attributes = savedAttributes
177
+ self.keys = savedKeys
178
+ self.element = savedElement
179
+ self.element.child += 1
180
+ }
181
+ }
182
+
183
+ div(body: () => Unit = {}) {self.add("div", body)}
184
+ span(body: () => Unit = {}) {self.add("span", body)}
185
+ label(body: () => Unit = {}) {self.add("label", body)}
186
+ button(body: () => Unit = {}) {self.add("button", body)}
187
+ form(body: () => Unit = {}) {self.add("form", body)}
188
+ input(body: () => Unit = {}) {self.add("input", body)}
189
+
190
+ keyed(key: String, body: () => Unit) {
191
+ try {
192
+ self.key = key
193
+ body()
194
+ } finally {
195
+ self.key = ""
196
+ }
197
+ }
198
+
199
+ keyedList[T](values: List[T], getKey: T => String, body: T => Unit) {
200
+ try {
201
+ values.each {item =>
202
+ self.key = getKey(item)
203
+ body(item)
204
+ }
205
+ } finally {
206
+ self.key = ""
207
+ }
208
+ }
209
+
210
+ set(attribute: String, value: String) {
211
+ let attributes = self.attributes.else {
212
+ let map = StringMap.new()
213
+ self.attributes = Some(map)
214
+ map
215
+ }
216
+ attributes.set(attribute, value)
217
+ }
218
+
219
+ setId(value: String) {self.set("id", value)}
220
+
221
+ setValue(value: String) { // TODO: Not an attribute
222
+ self.dry.map {_ => self.set("value", value)}.else:
223
+ self.element.element->value = value
224
+ }
225
+
226
+ css(style: Css) {
227
+ let attributes = self.attributes.else {
228
+ let map = StringMap.new()
229
+ self.attributes = Some(map)
230
+ map
231
+ }
232
+ if(attributes.has("style")) {
233
+ attributes.set("style", attributes.grab("style") + ";" + style.property + ":" + style.value)
234
+ } else {
235
+ attributes.set("style", style.property + ":" + style.value)
236
+ }
237
+ }
238
+
239
+ cssClass(class: CssClass) {
240
+ if(!self.cssClasses.has(class.name())) {
241
+ self.cssClasses.set(class.name(), class)
242
+ if(self.dry.isEmpty()):
243
+ let styleSheet = self.document.createElement("style")
244
+ styleSheet->textContent = class.show()
245
+ self.document.document->head->appendChild(styleSheet)
246
+ }
247
+ let classNames = self.attributes.flatMap {_.get("class")}.map {_ + " "}.else {""}
248
+ self.set("class", classNames + class.name())
249
+ }
250
+
251
+ on(event: String, handler: LuxEvent => Unit) {
252
+ if(self.dry.isEmpty()):
253
+ let jsHandler = Js->{jsEvent =>
254
+ self.renderLock.do {
255
+ handler(LuxEvent(jsEvent))
256
+ processRenderQueue(self)!
257
+ }
258
+ }
259
+ self.element.element->addEventListener(event, jsHandler)
260
+ if(self.element.element->luxHandlers.isNullOrUndefined()) {
261
+ self.element.element->luxHandlers = self.jsSystem.array([])
262
+ }
263
+ self.element.element->luxHandlers->push(
264
+ self.jsSystem.object().with("event", event).with("handler", jsHandler)
265
+ )
266
+ }
267
+
268
+ onClick(handler: LuxEvent => Unit) {self.on("click", handler)}
269
+ onInput(handler: LuxEvent => Unit) {self.on("input", handler)}
270
+
271
+ useState[T: HasAnyTag](initialValue: T, body: (T, T => Unit) => Unit) {
272
+ self.dry.map {_ => body(initialValue, {_ => })}.else:
273
+ self.depth += 1
274
+ let value = getStateOnElement(self.element.element, self.depth, initialValue)
275
+ mutable i = 0
276
+ while {i < self.renderQueue.size()} {
277
+ let item = self.renderQueue.grab(i)
278
+ if(item.luxCopy.element.element.equals(self.element.element) && item.luxCopy.depth == self.depth) {
279
+ self.renderQueue.delete(i, 1)
280
+ } else {
281
+ i += 1
282
+ }
283
+ }
284
+ let luxCopy = self.Lux(element = self.element.LuxElement())
285
+ function setState(newValue: T): Unit {
286
+ setStateOnElement(luxCopy.element.element, luxCopy.depth, newValue)
287
+ self.renderQueue.push(RenderQueueItem(
288
+ luxCopy = luxCopy
289
+ render = {
290
+ let child = luxCopy.element.child
291
+ let element = luxCopy.element.childAt(child)
292
+ body(newValue, {setState(_)})
293
+ if(!element.equals(luxCopy.element.childAt(child))) {
294
+ removeCurrentChild(luxCopy.Lux(
295
+ element = luxCopy.element.LuxElement(child = luxCopy.element.child + 1)
296
+ ))
297
+ }
298
+ }
299
+ ))
300
+ }
301
+ try {
302
+ body(value, {setState(_)})
303
+ } finally {
304
+ self.depth -= 1
305
+ }
306
+ }
307
+
308
+ useCallback1[A1: Equal: HasAnyTag](callback: A1 => Unit, body: (A1 => Unit) => Unit) {
309
+ self.dry.map {_ => body(callback)}.else:
310
+ self.depth += 1
311
+ setStateOnElement(self.element.element, self.depth, callback)
312
+ let element = self.element.element
313
+ let depth = self.depth
314
+ try {
315
+ body {a1 =>
316
+ let latestCallback = getStateOnElement(element, depth, callback)
317
+ latestCallback(a1)
318
+ }
319
+ } finally {
320
+ self.depth -= 1
321
+ }
322
+ }
323
+
324
+ useLazy1[A1: Equal: HasAnyTag](a1: A1, body: A1 => Unit = {_ => }) {
325
+ self.dry.map {_ => body(a1)}.else:
326
+ self.depth += 1
327
+ try {
328
+ let old = getStateOnElement(self.element.element, self.depth, None)
329
+ old.{
330
+ | Some(o1) {a1 == o1} =>
331
+ self.element.keepChildren = True
332
+ | _ =>
333
+ setStateOnElement(self.element.element, self.depth, Some(a1))
334
+ body(a1)
335
+ }
336
+ } finally {
337
+ self.depth -= 1
338
+ }
339
+ }
340
+
341
+ useMemo1[A1: Equal: HasAnyTag, T: HasAnyTag](a1: A1, compute: A1 => T, body: T => Unit) {
342
+ self.dry.map {_ => body(compute(a1))}.else:
343
+ self.depth += 1
344
+ try {
345
+ let old = getStateOnElement(self.element.element, self.depth, Pair(a1, None))
346
+ let computed = old.{
347
+ | Pair(o1, _) {a1 != o1} =>
348
+ let v = compute(a1)
349
+ setStateOnElement(self.element.element, self.depth, Pair(a1, Some(v)))
350
+ v
351
+ | Pair(_, Some(v)) =>
352
+ v
353
+ | Pair(_, None) =>
354
+ let v = compute(a1)
355
+ setStateOnElement(self.element.element, self.depth, Pair(a1, Some(v)))
356
+ v
357
+ }
358
+ body(computed)
359
+ } finally {
360
+ self.depth -= 1
361
+ }
362
+ }
363
+
364
+ useSuspense(suspense: () => Unit, body: Lux => Unit) {
365
+ self.dry.map {_ => suspense()}.else:
366
+ let oldSubtask = getTaskOnElement(self.element.element)
367
+ oldSubtask.each {task =>
368
+ forceAsyncAbort(self, task)
369
+ }
370
+ let fragment = self.document.createFragment()
371
+ let luxCopy = self.Lux(element = self.element.LuxElement())
372
+ let subtask = self.task.spawn {task =>
373
+ let lux = luxCopy.Lux(
374
+ task = task
375
+ renderLock = task.lock()
376
+ renderQueue = Array.new()
377
+ element = luxCopy.element.LuxElement(element = fragment)
378
+ )
379
+ try {
380
+ body(lux)
381
+ task.throwIfAborted()
382
+ lux.copyFrom(luxCopy)
383
+ lux.renderLock.do {
384
+ abortTasksOnElement(lux, lux.element.childAt(lux.element.child), onlyChildren = True)
385
+ lux.element.removeAt(lux.element.child)
386
+ lux.element.insertBefore(fragment, lux.jsSystem.null())
387
+ }
388
+ } catchAny {error =>
389
+ if(error.name() != "AbortError") {
390
+ error.rethrow()
391
+ }
392
+ }
393
+ }
394
+ suspense()
395
+ setTaskOnElement(self.element.childAt(self.element.child - 1), Some(subtask))
396
+ }
397
+
398
+ }
399
+
400
+ processRenderQueue(self: Lux) {
401
+ self.renderQueue.sortBy {-_.luxCopy.depth}
402
+ let newLux = self.Lux(element = self.element.LuxElement())
403
+ try {
404
+ while {!self.renderQueue.isEmpty()} {
405
+ let item = self.renderQueue.pop().grab()
406
+ self.copyFrom(item.luxCopy)
407
+ item.render()
408
+ }
409
+ } finally {
410
+ self.copyFrom(newLux)
411
+ }
412
+ }
413
+
414
+ removeCurrentChild(self: Lux): Bool {
415
+ let child = self.element.childAt(self.element.child)
416
+ if(!child.isNullOrUndefined() && !child->children.isNullOrUndefined()) {
417
+ abortTasksOnElement(self, child, False)
418
+ }
419
+ self.element.removeAt(self.element.child)
420
+ }
421
+
422
+ getStateOnElement[T /*: HasAnyTag*/](element: JsValue, depth: Int, fallback: T): T {
423
+ let value = element.get("lux" + depth)
424
+ if(value.isNullOrUndefined()) {fallback} else {
425
+ value?
426
+ }
427
+ }
428
+
429
+ setStateOnElement[T /*: HasAnyTag*/](element: JsValue, depth: Int, value: T): Unit {
430
+ element.set("lux" + depth, value!)
431
+ }
432
+
433
+
434
+ getTaskOnElement(element: JsValue): Option[Task] {
435
+ let value = element->luxTask
436
+ if(value.isNullOrUndefined()) {None} else {
437
+ value?
438
+ }
439
+ }
440
+
441
+ setTaskOnElement(element: JsValue, task: Option[Task]): Unit {
442
+ element->luxTask = task!
443
+ }
444
+
445
+ abortTasksOnElement(lux: Lux, element: JsValue, onlyChildren: Bool): Unit {
446
+ if(!onlyChildren) {
447
+ getTaskOnElement(element).each {task => forceAsyncAbort(lux, task)}
448
+ }
449
+ element->children.each {child =>
450
+ abortTasksOnElement(lux, child, False)
451
+ }
452
+ }
453
+
454
+ forceAsyncAbort(self: Lux, task: Task): Unit {
455
+ task.abort()
456
+ }
457
+
458
+ patchText(self: Lux) {
459
+ if(!self.texts.isEmpty()):
460
+ let value = self.texts.drain().join()
461
+ self.dry.map {_.push(DryText(value))}.else:
462
+ let oldNode = self.element.childAt(self.element.child)
463
+ let oldValue = if(!oldNode.isNullOrUndefined()) {oldNode->data} else {oldNode}
464
+ if(oldValue.isNullOrUndefined() || oldValue.grabString() != value) {
465
+ let node = self.document.createTextNode(value)
466
+ self.element.insertBefore(node, oldNode)
467
+ }
468
+ self.element.child += 1
469
+ }
470
+
471
+ patchElement(self: Lux, tagName: String): JsValue {
472
+ let newKey = if(self.key != "") {tagName.upper() + ">" + self.key} else {""}
473
+ let oldNode = self.element.childAt(self.element.child)
474
+ let oldKey = if(!oldNode.isNullOrUndefined() && !oldNode->getAttribute.isNullOrUndefined()) {
475
+ oldNode->getAttribute("data-lux-key")
476
+ } else {oldNode}
477
+ let match = if(newKey != "") {
478
+ !oldKey.isNullOrUndefined() &&
479
+ oldKey.grabString() == newKey
480
+ } else {
481
+ oldKey.isNullOrUndefined() &&
482
+ !oldNode.isNullOrUndefined() &&
483
+ !oldNode->tagName.isNullOrUndefined() &&
484
+ oldNode->tagName.grabString() == tagName.upper()
485
+ }
486
+ if(match) {
487
+ oldNode
488
+ } else {
489
+ let newNode = if(newKey == "") {self.document.createElement(tagName)} else {
490
+ let keys = self.keys.else {
491
+ let map = StringMap.new()
492
+ mutable i = self.element.child
493
+ mutable c = self.element.childAt(i)
494
+ while {!c.isNullOrUndefined()} {
495
+ if(!c->getAttribute.isNullOrUndefined()) {
496
+ let k = c->getAttribute("data-lux-key")
497
+ if(!k.isNullOrUndefined()) {map.set(k.grabString(), c)}
498
+ }
499
+ i += 1
500
+ c = self.element.childAt(i)
501
+ }
502
+ self.keys = Some(map)
503
+ map
504
+ }
505
+ if(keys.has(newKey)) {
506
+ let foundNode = keys.grab(newKey)
507
+ keys.remove(newKey)
508
+ foundNode
509
+ } else {
510
+ let createdNode = self.document.createElement(tagName)
511
+ createdNode->setAttribute("data-lux-key", newKey)
512
+ createdNode
513
+ }
514
+ }
515
+ self.element.insertBefore(newNode, oldNode)
516
+ newNode
517
+ }
518
+ }
519
+
520
+ patchAttributes(self: Lux) {
521
+ let attributes = self.element.element->attributes
522
+ self.attributes.{
523
+ | None =>
524
+ mutable i = attributes->length.grabInt() - 1
525
+ while {i >= 0} {
526
+ let attribute = attributes.get(i)
527
+ self.element.element->removeAttribute(attribute->name)
528
+ i -= 1
529
+ }
530
+ | Some(map) =>
531
+ mutable i = attributes->length.grabInt() - 1
532
+ while {i >= 0} {
533
+ let attribute = attributes.get(i)
534
+ if(!map.has(attribute->name.grabString())) {
535
+ self.element.element->removeAttribute(attribute->name)
536
+ }
537
+ i -= 1
538
+ }
539
+ map.each {name, value =>
540
+ let oldValue = self.element.element->getAttribute(name)
541
+ if(oldValue.isNullOrUndefined() || !oldValue.equals(value)) {
542
+ self.element.element->setAttribute(name, value)
543
+ }
544
+ }
545
+ }
546
+ }
547
+
548
+ render(browserSystem: BrowserSystem, element: JsValue, body: Lux => Unit) {
549
+ mutable document = element
550
+ while {!document->parentNode.isNullOrUndefined()} {
551
+ document = document->parentNode
552
+ }
553
+ // [...document.querySelectorAll("[lux-class]")].map(x => x.getAttribute("lux-class"))
554
+ let staticCssClasses = StringMap.new()
555
+ let dummyCssClass = CssClass([], [], [])
556
+ document->querySelectorAll("[lux-class]").each {e =>
557
+ staticCssClasses.set(e->getAttribute("lux-class").grabString(), dummyCssClass)
558
+ }
559
+ let lux = Lux(
560
+ jsSystem = browserSystem.js()
561
+ renderLock = browserSystem.mainTask().lock()
562
+ dry = None
563
+ cssClasses = staticCssClasses
564
+ task = browserSystem.mainTask()
565
+ depth = 0
566
+ document = LuxDocument(document)
567
+ element = LuxElement(element, 0, keepChildren = False)
568
+ keys = None
569
+ key = ""
570
+ attributes = None
571
+ texts = Array.new()
572
+ renderQueue = Array.new()
573
+ )
574
+ lux.renderLock.do {
575
+ body(lux)
576
+ patchText(lux)
577
+ }
578
+ }
579
+
580
+ renderById(browserSystem: BrowserSystem, id: String, body: Lux => Unit) {
581
+ let element = browserSystem.js()->document->getElementById(id)
582
+ render(browserSystem, element, body)
583
+ }
584
+
585
+ renderToString(nodeSystem: NodeSystem, body: Lux => Unit): Pair[String, String] {
586
+ let children = [].toArray()
587
+ let lux = Lux(
588
+ jsSystem = nodeSystem.js()
589
+ renderLock = nodeSystem.mainTask().lock()
590
+ dry = Some(children)
591
+ cssClasses = StringMap.new()
592
+ task = nodeSystem.mainTask()
593
+ depth = 0
594
+ document = LuxDocument(nodeSystem.js().null())
595
+ element = LuxElement(nodeSystem.js().null(), 0, keepChildren = False)
596
+ keys = None
597
+ key = ""
598
+ attributes = None
599
+ texts = Array.new()
600
+ renderQueue = Array.new()
601
+ )
602
+ body(lux)
603
+ patchText(lux)
604
+ let styleTags = lux.cssClasses.values().map {c =>
605
+ "<style lux-class=\"" + c.name() + "\">" + c.show() + "</style>"
606
+ }.join()
607
+ Pair(children.toList().map {_.toHtml()}.join(), styleTags)
608
+ }