firefly-compiler 0.5.38 → 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 (128) 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 -276
  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 -377
  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 -38
  107. package/package.json +1 -1
  108. package/rpc/.firefly/package.ff +1 -1
  109. package/rpc/Rpc.ff +70 -70
  110. package/s3/.firefly/package.ff +1 -1
  111. package/s3/S3.ff +92 -92
  112. package/vscode/LICENSE.txt +21 -21
  113. package/vscode/Prepublish.ff +15 -15
  114. package/vscode/README.md +16 -16
  115. package/vscode/client/package-lock.json +544 -544
  116. package/vscode/client/package.json +22 -22
  117. package/vscode/client/src/extension.ts +104 -104
  118. package/vscode/icons/firefly-icon.svg +10 -10
  119. package/vscode/language-configuration.json +61 -61
  120. package/vscode/package-lock.json +3623 -3623
  121. package/vscode/package.json +1 -1
  122. package/vscode/snippets.json +241 -241
  123. package/vscode/syntaxes/firefly-markdown-injection.json +45 -45
  124. package/webserver/.firefly/include/package.json +5 -5
  125. package/webserver/.firefly/package.ff +2 -2
  126. package/webserver/WebServer.ff +647 -647
  127. package/websocket/.firefly/package.ff +1 -1
  128. package/websocket/WebSocket.ff +100 -100
@@ -1,456 +1,456 @@
1
- import Lux from ff:lux
2
- import LuxEvent from ff:lux
3
- import Css from ff:lux
4
- import Tokenizer from ff:compiler
5
- import Token from ff:compiler
6
- import Styles
7
- import DocumentParser
8
-
9
- data Guide(
10
- prefix: String
11
- documents: List[Document]
12
- )
13
-
14
- data Document {
15
- ReadyDocument(
16
- sections: List[Section]
17
- )
18
- UnfetchedDocument(
19
- header: String
20
- )
21
- }
22
-
23
-
24
- data Section(heading: String) {
25
- Section(
26
- blocks: List[Block]
27
- )
28
- SplitSection(
29
- first: Block
30
- second: Block
31
- )
32
- }
33
-
34
- data Block {
35
- Paragraph(inlines: List[Inline])
36
- Bullets(items: List[List[Inline]])
37
- CodeBlock(code: String, firefly: Bool = False)
38
- Image(url: String)
39
- Video(url: String)
40
- LuxDemo(demo: String)
41
- }
42
-
43
- data Inline {
44
- Text(text: String)
45
- Bold(text: String)
46
- Italic(text: String)
47
- Code(code: String, firefly: Bool = False)
48
- Link(text: String, url: String)
49
- Anchor(heading: String, title: Option[String] = None)
50
- }
51
-
52
- extend self: Guide {
53
- heading(): String {
54
- self.documents.first().map {_.heading()}.else {""}
55
- }
56
- title(): String {
57
- if(self.heading() == "Firefly") {self.heading()} else {"Firefly " + self.heading()}
58
- }
59
- }
60
-
61
- extend self: Document {
62
- heading(): String {
63
- self.{
64
- | ReadyDocument(sections) =>
65
- sections.first().map {_.heading}.else {""}
66
- | UnfetchedDocument(header) => header
67
- }
68
- }
69
- title(guide: Guide): String {
70
- if(guide.documents.first().any {_.heading() == self.heading()}) {
71
- guide.title()
72
- } else {
73
- self.heading() + " · " + guide.title()
74
- }
75
- }
76
- path(guide: Guide): String {
77
- if(guide.documents.first().any {_.heading() == self.heading()}) {
78
- guide.prefix
79
- } else {
80
- guide.prefix + kebabCase(self.heading())
81
- }
82
- }
83
- }
84
-
85
- capability Demo(
86
- name: String
87
- render: Lux => Unit
88
- )
89
-
90
- data GuideDocument(
91
- guide: Guide
92
- document: Document
93
- )
94
-
95
- render(lux: Lux, httpClient: HttpClient, prefix: String, kebab: String, guides: List[Guide], demos: List[Demo]) {
96
- let guide = guides.find {_.prefix == prefix}.else {guides.grabFirst()}
97
- let document = guide.documents.find {d =>
98
- kebabCase(d.heading()) == kebab
99
- }.else {guide.documents.grabFirst()}
100
- let guideDocuments = guides.flatMap {guide => guide.documents.map {document =>
101
- GuideDocument(guide, document)
102
- }}
103
- let nextDocument = guideDocuments.dropWhile {n =>
104
- n.guide.prefix != guide.prefix || n.document.heading() != document.heading()
105
- }.dropFirst().first()
106
- lux.add("div") {
107
- lux.useState(False): menu, setMenu =>
108
- lux.cssClass(Styles.pageCss)
109
- lux.add("div") {
110
- lux.cssClass(Styles.guideCss)
111
- lux.add("main") {
112
- lux.cssClass(Styles.guideMainCss)
113
- renderDocument(lux, httpClient, prefix, document, demos, nextDocument)
114
- }
115
- renderTopbar(lux, menu, setMenu)
116
- lux.add("div") {
117
- lux.cssClass(Styles.guideSidebarBackdropCss)
118
- if(menu) {lux.cssClass(Styles.guideSidebarBackdropOpenCss)}
119
- lux.onClick {event =>
120
- event.preventDefault()
121
- setMenu(False)
122
- }
123
- }
124
- renderSidebar(lux, guides, guide, document.heading(), menu)
125
- }
126
- }
127
- }
128
-
129
- renderTopbar(lux: Lux, menu: Bool, setMenu: Bool => Unit) {
130
- lux.add("div") {
131
- lux.cssClass(Styles.guideTopbarCss)
132
- lux.add("a") {
133
- lux.set("href", "/")
134
- lux.cssClass(Styles.guideTopbarLogoCss)
135
- lux.add("img") {
136
- lux.set("src", "/assets/image/firefly-logo-yellow.webp")
137
- }
138
- lux.div {
139
- lux.text("Firefly")
140
- }
141
- }
142
- lux.add("button") {
143
- lux.set("aria-label", "Toggle the menu")
144
- lux.cssClass(Styles.guideTopbarButtonCss)
145
- lux.onClick {event =>
146
- event.preventDefault()
147
- setMenu(True)
148
- }
149
- }
150
- }
151
- }
152
-
153
- renderSidebar(lux: Lux, guides: List[Guide], selectedGuide: Guide, selectedHeading: String, menu: Bool) {
154
- lux.add("nav") {
155
- lux.cssClass(Styles.guideSidebarCss)
156
- if(menu) {lux.cssClass(Styles.guideSidebarOpenCss)}
157
- lux.add("a") {
158
- lux.set("href", "/")
159
- lux.cssClass(Styles.guideSidebarLogoCss)
160
- lux.add("img") {
161
- lux.set("src", "/assets/image/firefly-logo-yellow.webp")
162
- }
163
- lux.div {
164
- lux.text("Firefly")
165
- }
166
- }
167
- lux.add("form") {
168
- lux.set("role", "search")
169
- lux.add("input") {
170
- lux.set("aria-label", "Search")
171
- lux.cssClass(Styles.searchInputCss)
172
- lux.set("placeholder", "Search...")
173
- }
174
- }
175
- lux.add("ul") {
176
- lux.cssClass(Styles.guideSidebarUl1Css)
177
- guides.dropFirst().each {guide =>
178
- lux.add("li") {
179
- lux.cssClass(Styles.guideSidebarLi1Css)
180
- lux.add("a") {
181
- lux.cssClass(Styles.whiteLinkCss)
182
- let heading = guide.documents.grabFirst().heading()
183
- if(selectedGuide.prefix == guide.prefix && heading == selectedHeading) {
184
- lux.set("aria-current", "page")
185
- }
186
- lux.set("href", guide.prefix)
187
- lux.text(heading)
188
- }
189
- if(guide.documents.size() > 1):
190
- lux.add("ul") {
191
- lux.cssClass(Styles.guideSidebarUl2Css)
192
- guide.documents.dropFirst().each {document =>
193
- lux.add("li") {
194
- lux.cssClass(Styles.guideSidebarLi2Css)
195
- lux.add("a") {
196
- lux.cssClass(Styles.whiteLinkCss)
197
- let heading = document.heading()
198
- if(selectedGuide.prefix == guide.prefix && heading == selectedHeading) {
199
- lux.set("aria-current", "page")
200
- }
201
- lux.set("href", guide.prefix + kebabCase(heading))
202
- lux.text(heading)
203
- }
204
- }
205
- }
206
- }
207
- }
208
- }
209
- }
210
- }
211
- }
212
-
213
- renderDocument(
214
- lux: Lux
215
- http: HttpClient
216
- prefix: String
217
- document: Document
218
- demos: List[Demo]
219
- nextDocument: Option[GuideDocument]
220
- ) {
221
- lux.add("article") {
222
- lux.cssClass(Styles.guideDocumentCss)
223
- document.{
224
- | ReadyDocument(sections) =>
225
- renderSections(lux, sections, demos)
226
- renderNext(lux, nextDocument)
227
- | UnfetchedDocument(header) =>
228
- lux.div {
229
- lux.useSuspense {lux.div {lux.text("Loading document...")}}: lux =>
230
- let firstName = document.heading().split(' ').map {w =>
231
- let word = w.trim().lower()
232
- word.slice(0, 1).upper() + word.dropFirst()
233
- }.join()
234
- let asset = "/assets/markdown" + prefix + firstName + ".md"
235
- let markdown = http.get(asset, []) {_.readText()}
236
- let parser = DocumentParser(asset, markdown.split('\n'), 0)
237
- let sections = parser.parseDocument()
238
- renderSections(lux, sections, demos)
239
- renderNext(lux, nextDocument)
240
- }
241
- }
242
- }
243
- }
244
-
245
- renderSections(lux: Lux, sections: List[Section], demos: List[Demo]) {
246
- sections.pairs().each {| Pair(index, section) =>
247
- renderSection(lux, index == 0, section, demos)
248
- }
249
- }
250
-
251
- renderNext(lux: Lux, nextDocument: Option[GuideDocument]) {
252
- nextDocument.each {next =>
253
- lux.div {
254
- lux.cssClass(Styles.guideNextButtonCss)
255
- lux.add("a") {
256
- lux.cssClass(Styles.guideButtonCss)
257
- lux.set("href", next.document.path(next.guide))
258
- lux.text("Next: " + next.document.heading())
259
- }
260
- }
261
- }
262
- }
263
-
264
- renderSection(lux: Lux, firstSection: Bool, section: Section, demos: List[Demo]) {
265
- section.{
266
- | Section(heading, blocks) =>
267
- lux.add(if(firstSection) {"h1"} else {"h2"}) {
268
- lux.cssClass(if(firstSection) {Styles.guideH1Css} else {Styles.guideH2Css})
269
- lux.text(heading)
270
- }
271
- blocks.each {renderBlock(lux, _, demos)}
272
- | SplitSection(heading, first, second) =>
273
- lux.add("div") {
274
- lux.cssClass(Styles.guideSplitCss)
275
- lux.add("div") {
276
- lux.add("h2") {
277
- lux.cssClass(Styles.guideSplitHeadingCss)
278
- lux.text(section.heading)
279
- }
280
- renderBlock(lux, first, demos)
281
- }
282
- lux.add("div") {
283
- renderBlock(lux, second, demos)
284
- }
285
- }
286
- }
287
- }
288
-
289
- renderBlock(lux: Lux, block: Block, demos: List[Demo]) {
290
- block.{
291
- | Paragraph(inlines) => renderParagraph(lux, inlines)
292
- | CodeBlock(code, firefly) => renderCodeBlock(lux, code, firefly)
293
- | Bullets(items) => renderBullets(lux, items)
294
- | Image(url) => renderImage(lux, url)
295
- | Video(url) => renderVideo(lux, url)
296
- | LuxDemo(name) => renderLuxDemo(lux, name, demos)
297
- }
298
- }
299
-
300
- renderParagraph(lux: Lux, inlines: List[Inline]) {
301
- lux.add("p") {
302
- renderInlines(lux, inlines)
303
- }
304
- }
305
-
306
- renderCodeBlock(lux: Lux, code: String, firefly: Bool) {
307
- let lines = code.lines().dropWhile {_.all {_ == ' '}}.reverse().dropWhile {_.all {_ == ' '}}.reverse()
308
- let indentation = lines.first().map {_.takeWhile {_ == ' '}.size()}.else {0}
309
- lux.add("pre") {
310
- lux.cssClass(Styles.guideCodeBlockCss)
311
- lux.add("code") {
312
- lux.cssClass(Styles.guideCodeCss)
313
- let unindentedCode = lines.map {_.dropFirst(indentation)}.join("\n")
314
- if(firefly) {
315
- renderHighlightedCode(lux, unindentedCode)
316
- } else {
317
- lux.text(unindentedCode)
318
- }
319
- }
320
- }
321
- }
322
-
323
- renderBullets(lux: Lux, items: List[List[Inline]]) {
324
- lux.add("ul") {
325
- items.each {inlines =>
326
- lux.add("li") {
327
- renderInlines(lux, inlines)
328
- }
329
- }
330
- }
331
- }
332
-
333
- renderImage(lux: Lux, url: String) {
334
- lux.add("img") {
335
- lux.set("src", url)
336
- lux.css(Css.maxWidth("100%"))
337
- lux.css(Css.borderRadius("8px"))
338
- }
339
- }
340
-
341
- renderVideo(lux: Lux, url: String) {
342
- lux.add("video") {
343
- lux.set("src", url)
344
- lux.css(Css.maxWidth("100%"))
345
- }
346
- }
347
-
348
- renderLuxDemo(lux: Lux, name: String, demos: List[Demo]) {
349
- demos.find {_.name == name}.map {_.render(lux)}.else {lux.text("Demo not found")}
350
- }
351
-
352
- renderInlines(lux: Lux, inlines: List[Inline]) {
353
- inlines.separate([Text(" ")]).each {renderInline(lux, _)}
354
- }
355
-
356
- renderInline(lux: Lux, inline: Inline) {
357
- inline.{
358
- | Anchor(heading, title) =>
359
- lux.add("a") {
360
- let url = title.map {"/guide/" + kebabCase(_)}.else {""} + "#" + kebabCase(heading)
361
- lux.cssClass(Styles.guideLinkCss)
362
- lux.set("href", url)
363
- lux.text(title.map {_ + ": "}.else {""} + heading)
364
- }
365
- | Bold(text) =>
366
- lux.add("b") {
367
- lux.text(text)
368
- }
369
- | Code(code, firefly) =>
370
- lux.add("code") {
371
- lux.cssClass(Styles.guideCodeCss)
372
- if(firefly) {
373
- renderHighlightedCode(lux, code)
374
- } else {
375
- lux.text(code)
376
- }
377
- }
378
- | Italic(text) =>
379
- lux.add("i") {
380
- lux.text(text)
381
- }
382
- | Link(text, url) =>
383
- lux.add("a") {
384
- lux.cssClass(Styles.guideLinkCss)
385
- lux.set("href", url)
386
- lux.text(text)
387
- }
388
- | Text(text) =>
389
- lux.text(text)
390
- }
391
- }
392
-
393
- renderHighlightedCode(lux: Lux, code: String) {
394
- try {
395
- Tokenizer.tokenize("<example>", code + "\n", None, False)
396
- }.toOption().map {tokens =>
397
- mutable offset = 0
398
- mutable index = 0
399
- lux.span {
400
- lux.cssClass(Styles.codeOtherCss)
401
- tokens.each {token =>
402
- if(token.startOffset > offset && index <= tokens.size() - 5) {
403
- let text = code.slice(offset, token.startOffset)
404
- if(text.all {c => c == ' ' || c == '\n'}) {
405
- lux.text(text)
406
- } else {
407
- lux.span {
408
- lux.cssClass(Styles.codeCommentCss)
409
- lux.text(text)
410
- }
411
- }
412
- }
413
- if(token.kind != LEnd) {
414
- let css = token.kind.{
415
- | LChar => Styles.codeStringCss
416
- | LFloat => Styles.codeNumberCss
417
- | LInt => Styles.codeNumberCss
418
- | LKeyword => Styles.codeKeywordCss
419
- | LNamespace => Styles.codeTypeCss
420
- | LString => Styles.codeStringCss
421
- | LWildcard => Styles.codeVariableCss
422
- | LUpper {tokens.get(index - 1).any {_.kind == LArrowThin}} => Styles.codeStringCss
423
- | LUpper => Styles.codeTypeCss
424
- | LLower {tokens.get(index - 1).any {_.kind == LArrowThin}} => Styles.codeStringCss
425
- | LLower {tokens.get(index - 1).any {t =>
426
- t.kind == LBracketRight || t.kind == LDot
427
- }} => Styles.codeCallCss
428
- | LLower {tokens.grab(index + 1).kind == LBracketLeft} => Styles.codeCallCss
429
- | LLower => Styles.codeVariableCss
430
- | _ => Styles.codeOtherCss
431
- }
432
- if(css == Styles.codeOtherCss) {
433
- lux.text(code.slice(token.startOffset, token.stopOffset))
434
- } else {
435
- lux.span {
436
- lux.cssClass(css)
437
- lux.text(code.slice(token.startOffset, token.stopOffset))
438
- }
439
- }
440
- offset = token.stopOffset
441
- }
442
- index += 1
443
- }
444
- }
445
- }.else {lux.text(code)}
446
- }
447
-
448
- kebabCase(text: String): String {
449
- mutable result = text.trim().lower().filter {c => c.isAsciiLetterOrDigit() || c == ' '}
450
- doWhile {
451
- let before = result
452
- result = result.replace(" ", "")
453
- result != before
454
- }
455
- result.replace(" ", "-")
456
- }
1
+ import Lux from ff:lux
2
+ import LuxEvent from ff:lux
3
+ import Css from ff:lux
4
+ import Tokenizer from ff:compiler
5
+ import Token from ff:compiler
6
+ import Styles
7
+ import DocumentParser
8
+
9
+ data Guide(
10
+ prefix: String
11
+ documents: List[Document]
12
+ )
13
+
14
+ data Document {
15
+ ReadyDocument(
16
+ sections: List[Section]
17
+ )
18
+ UnfetchedDocument(
19
+ header: String
20
+ )
21
+ }
22
+
23
+
24
+ data Section(heading: String) {
25
+ Section(
26
+ blocks: List[Block]
27
+ )
28
+ SplitSection(
29
+ first: Block
30
+ second: Block
31
+ )
32
+ }
33
+
34
+ data Block {
35
+ Paragraph(inlines: List[Inline])
36
+ Bullets(items: List[List[Inline]])
37
+ CodeBlock(code: String, firefly: Bool = False)
38
+ Image(url: String)
39
+ Video(url: String)
40
+ LuxDemo(demo: String)
41
+ }
42
+
43
+ data Inline {
44
+ Text(text: String)
45
+ Bold(text: String)
46
+ Italic(text: String)
47
+ Code(code: String, firefly: Bool = False)
48
+ Link(text: String, url: String)
49
+ Anchor(heading: String, title: Option[String] = None)
50
+ }
51
+
52
+ extend self: Guide {
53
+ heading(): String {
54
+ self.documents.first().map {_.heading()}.else {""}
55
+ }
56
+ title(): String {
57
+ if(self.heading() == "Firefly") {self.heading()} else {"Firefly " + self.heading()}
58
+ }
59
+ }
60
+
61
+ extend self: Document {
62
+ heading(): String {
63
+ self.{
64
+ | ReadyDocument(sections) =>
65
+ sections.first().map {_.heading}.else {""}
66
+ | UnfetchedDocument(header) => header
67
+ }
68
+ }
69
+ title(guide: Guide): String {
70
+ if(guide.documents.first().any {_.heading() == self.heading()}) {
71
+ guide.title()
72
+ } else {
73
+ self.heading() + " · " + guide.title()
74
+ }
75
+ }
76
+ path(guide: Guide): String {
77
+ if(guide.documents.first().any {_.heading() == self.heading()}) {
78
+ guide.prefix
79
+ } else {
80
+ guide.prefix + kebabCase(self.heading())
81
+ }
82
+ }
83
+ }
84
+
85
+ capability Demo(
86
+ name: String
87
+ render: Lux => Unit
88
+ )
89
+
90
+ data GuideDocument(
91
+ guide: Guide
92
+ document: Document
93
+ )
94
+
95
+ render(lux: Lux, httpClient: HttpClient, prefix: String, kebab: String, guides: List[Guide], demos: List[Demo]) {
96
+ let guide = guides.find {_.prefix == prefix}.else {guides.grabFirst()}
97
+ let document = guide.documents.find {d =>
98
+ kebabCase(d.heading()) == kebab
99
+ }.else {guide.documents.grabFirst()}
100
+ let guideDocuments = guides.flatMap {guide => guide.documents.map {document =>
101
+ GuideDocument(guide, document)
102
+ }}
103
+ let nextDocument = guideDocuments.dropWhile {n =>
104
+ n.guide.prefix != guide.prefix || n.document.heading() != document.heading()
105
+ }.dropFirst().first()
106
+ lux.add("div") {
107
+ lux.useState(False): menu, setMenu =>
108
+ lux.cssClass(Styles.pageCss)
109
+ lux.add("div") {
110
+ lux.cssClass(Styles.guideCss)
111
+ lux.add("main") {
112
+ lux.cssClass(Styles.guideMainCss)
113
+ renderDocument(lux, httpClient, prefix, document, demos, nextDocument)
114
+ }
115
+ renderTopbar(lux, menu, setMenu)
116
+ lux.add("div") {
117
+ lux.cssClass(Styles.guideSidebarBackdropCss)
118
+ if(menu) {lux.cssClass(Styles.guideSidebarBackdropOpenCss)}
119
+ lux.onClick {event =>
120
+ event.preventDefault()
121
+ setMenu(False)
122
+ }
123
+ }
124
+ renderSidebar(lux, guides, guide, document.heading(), menu)
125
+ }
126
+ }
127
+ }
128
+
129
+ renderTopbar(lux: Lux, menu: Bool, setMenu: Bool => Unit) {
130
+ lux.add("div") {
131
+ lux.cssClass(Styles.guideTopbarCss)
132
+ lux.add("a") {
133
+ lux.set("href", "/")
134
+ lux.cssClass(Styles.guideTopbarLogoCss)
135
+ lux.add("img") {
136
+ lux.set("src", "/assets/image/firefly-logo-yellow.webp")
137
+ }
138
+ lux.div {
139
+ lux.text("Firefly")
140
+ }
141
+ }
142
+ lux.add("button") {
143
+ lux.set("aria-label", "Toggle the menu")
144
+ lux.cssClass(Styles.guideTopbarButtonCss)
145
+ lux.onClick {event =>
146
+ event.preventDefault()
147
+ setMenu(True)
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ renderSidebar(lux: Lux, guides: List[Guide], selectedGuide: Guide, selectedHeading: String, menu: Bool) {
154
+ lux.add("nav") {
155
+ lux.cssClass(Styles.guideSidebarCss)
156
+ if(menu) {lux.cssClass(Styles.guideSidebarOpenCss)}
157
+ lux.add("a") {
158
+ lux.set("href", "/")
159
+ lux.cssClass(Styles.guideSidebarLogoCss)
160
+ lux.add("img") {
161
+ lux.set("src", "/assets/image/firefly-logo-yellow.webp")
162
+ }
163
+ lux.div {
164
+ lux.text("Firefly")
165
+ }
166
+ }
167
+ lux.add("form") {
168
+ lux.set("role", "search")
169
+ lux.add("input") {
170
+ lux.set("aria-label", "Search")
171
+ lux.cssClass(Styles.searchInputCss)
172
+ lux.set("placeholder", "Search...")
173
+ }
174
+ }
175
+ lux.add("ul") {
176
+ lux.cssClass(Styles.guideSidebarUl1Css)
177
+ guides.dropFirst().each {guide =>
178
+ lux.add("li") {
179
+ lux.cssClass(Styles.guideSidebarLi1Css)
180
+ lux.add("a") {
181
+ lux.cssClass(Styles.whiteLinkCss)
182
+ let heading = guide.documents.grabFirst().heading()
183
+ if(selectedGuide.prefix == guide.prefix && heading == selectedHeading) {
184
+ lux.set("aria-current", "page")
185
+ }
186
+ lux.set("href", guide.prefix)
187
+ lux.text(heading)
188
+ }
189
+ if(guide.documents.size() > 1):
190
+ lux.add("ul") {
191
+ lux.cssClass(Styles.guideSidebarUl2Css)
192
+ guide.documents.dropFirst().each {document =>
193
+ lux.add("li") {
194
+ lux.cssClass(Styles.guideSidebarLi2Css)
195
+ lux.add("a") {
196
+ lux.cssClass(Styles.whiteLinkCss)
197
+ let heading = document.heading()
198
+ if(selectedGuide.prefix == guide.prefix && heading == selectedHeading) {
199
+ lux.set("aria-current", "page")
200
+ }
201
+ lux.set("href", guide.prefix + kebabCase(heading))
202
+ lux.text(heading)
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ renderDocument(
214
+ lux: Lux
215
+ http: HttpClient
216
+ prefix: String
217
+ document: Document
218
+ demos: List[Demo]
219
+ nextDocument: Option[GuideDocument]
220
+ ) {
221
+ lux.add("article") {
222
+ lux.cssClass(Styles.guideDocumentCss)
223
+ document.{
224
+ | ReadyDocument(sections) =>
225
+ renderSections(lux, sections, demos)
226
+ renderNext(lux, nextDocument)
227
+ | UnfetchedDocument(header) =>
228
+ lux.div {
229
+ lux.useSuspense {lux.div {lux.text("Loading document...")}}: lux =>
230
+ let firstName = document.heading().split(' ').map {w =>
231
+ let word = w.trim().lower()
232
+ word.slice(0, 1).upper() + word.dropFirst()
233
+ }.join()
234
+ let asset = "/assets/markdown" + prefix + firstName + ".md"
235
+ let markdown = http.get(asset, []) {_.readText()}
236
+ let parser = DocumentParser(asset, markdown.split('\n'), 0)
237
+ let sections = parser.parseDocument()
238
+ renderSections(lux, sections, demos)
239
+ renderNext(lux, nextDocument)
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ renderSections(lux: Lux, sections: List[Section], demos: List[Demo]) {
246
+ sections.pairs().each {| Pair(index, section) =>
247
+ renderSection(lux, index == 0, section, demos)
248
+ }
249
+ }
250
+
251
+ renderNext(lux: Lux, nextDocument: Option[GuideDocument]) {
252
+ nextDocument.each {next =>
253
+ lux.div {
254
+ lux.cssClass(Styles.guideNextButtonCss)
255
+ lux.add("a") {
256
+ lux.cssClass(Styles.guideButtonCss)
257
+ lux.set("href", next.document.path(next.guide))
258
+ lux.text("Next: " + next.document.heading())
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ renderSection(lux: Lux, firstSection: Bool, section: Section, demos: List[Demo]) {
265
+ section.{
266
+ | Section(heading, blocks) =>
267
+ lux.add(if(firstSection) {"h1"} else {"h2"}) {
268
+ lux.cssClass(if(firstSection) {Styles.guideH1Css} else {Styles.guideH2Css})
269
+ lux.text(heading)
270
+ }
271
+ blocks.each {renderBlock(lux, _, demos)}
272
+ | SplitSection(heading, first, second) =>
273
+ lux.add("div") {
274
+ lux.cssClass(Styles.guideSplitCss)
275
+ lux.add("div") {
276
+ lux.add("h2") {
277
+ lux.cssClass(Styles.guideSplitHeadingCss)
278
+ lux.text(section.heading)
279
+ }
280
+ renderBlock(lux, first, demos)
281
+ }
282
+ lux.add("div") {
283
+ renderBlock(lux, second, demos)
284
+ }
285
+ }
286
+ }
287
+ }
288
+
289
+ renderBlock(lux: Lux, block: Block, demos: List[Demo]) {
290
+ block.{
291
+ | Paragraph(inlines) => renderParagraph(lux, inlines)
292
+ | CodeBlock(code, firefly) => renderCodeBlock(lux, code, firefly)
293
+ | Bullets(items) => renderBullets(lux, items)
294
+ | Image(url) => renderImage(lux, url)
295
+ | Video(url) => renderVideo(lux, url)
296
+ | LuxDemo(name) => renderLuxDemo(lux, name, demos)
297
+ }
298
+ }
299
+
300
+ renderParagraph(lux: Lux, inlines: List[Inline]) {
301
+ lux.add("p") {
302
+ renderInlines(lux, inlines)
303
+ }
304
+ }
305
+
306
+ renderCodeBlock(lux: Lux, code: String, firefly: Bool) {
307
+ let lines = code.lines().dropWhile {_.all {_ == ' '}}.reverse().dropWhile {_.all {_ == ' '}}.reverse()
308
+ let indentation = lines.first().map {_.takeWhile {_ == ' '}.size()}.else {0}
309
+ lux.add("pre") {
310
+ lux.cssClass(Styles.guideCodeBlockCss)
311
+ lux.add("code") {
312
+ lux.cssClass(Styles.guideCodeCss)
313
+ let unindentedCode = lines.map {_.dropFirst(indentation)}.join("\n")
314
+ if(firefly) {
315
+ renderHighlightedCode(lux, unindentedCode)
316
+ } else {
317
+ lux.text(unindentedCode)
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ renderBullets(lux: Lux, items: List[List[Inline]]) {
324
+ lux.add("ul") {
325
+ items.each {inlines =>
326
+ lux.add("li") {
327
+ renderInlines(lux, inlines)
328
+ }
329
+ }
330
+ }
331
+ }
332
+
333
+ renderImage(lux: Lux, url: String) {
334
+ lux.add("img") {
335
+ lux.set("src", url)
336
+ lux.css(Css.maxWidth("100%"))
337
+ lux.css(Css.borderRadius("8px"))
338
+ }
339
+ }
340
+
341
+ renderVideo(lux: Lux, url: String) {
342
+ lux.add("video") {
343
+ lux.set("src", url)
344
+ lux.css(Css.maxWidth("100%"))
345
+ }
346
+ }
347
+
348
+ renderLuxDemo(lux: Lux, name: String, demos: List[Demo]) {
349
+ demos.find {_.name == name}.map {_.render(lux)}.else {lux.text("Demo not found")}
350
+ }
351
+
352
+ renderInlines(lux: Lux, inlines: List[Inline]) {
353
+ inlines.separate([Text(" ")]).each {renderInline(lux, _)}
354
+ }
355
+
356
+ renderInline(lux: Lux, inline: Inline) {
357
+ inline.{
358
+ | Anchor(heading, title) =>
359
+ lux.add("a") {
360
+ let url = title.map {"/guide/" + kebabCase(_)}.else {""} + "#" + kebabCase(heading)
361
+ lux.cssClass(Styles.guideLinkCss)
362
+ lux.set("href", url)
363
+ lux.text(title.map {_ + ": "}.else {""} + heading)
364
+ }
365
+ | Bold(text) =>
366
+ lux.add("b") {
367
+ lux.text(text)
368
+ }
369
+ | Code(code, firefly) =>
370
+ lux.add("code") {
371
+ lux.cssClass(Styles.guideCodeCss)
372
+ if(firefly) {
373
+ renderHighlightedCode(lux, code)
374
+ } else {
375
+ lux.text(code)
376
+ }
377
+ }
378
+ | Italic(text) =>
379
+ lux.add("i") {
380
+ lux.text(text)
381
+ }
382
+ | Link(text, url) =>
383
+ lux.add("a") {
384
+ lux.cssClass(Styles.guideLinkCss)
385
+ lux.set("href", url)
386
+ lux.text(text)
387
+ }
388
+ | Text(text) =>
389
+ lux.text(text)
390
+ }
391
+ }
392
+
393
+ renderHighlightedCode(lux: Lux, code: String) {
394
+ try {
395
+ Tokenizer.tokenize("<example>", code + "\n", None, False)
396
+ }.toOption().map {tokens =>
397
+ mutable offset = 0
398
+ mutable index = 0
399
+ lux.span {
400
+ lux.cssClass(Styles.codeOtherCss)
401
+ tokens.each {token =>
402
+ if(token.startOffset > offset && index <= tokens.size() - 5) {
403
+ let text = code.slice(offset, token.startOffset)
404
+ if(text.all {c => c == ' ' || c == '\n'}) {
405
+ lux.text(text)
406
+ } else {
407
+ lux.span {
408
+ lux.cssClass(Styles.codeCommentCss)
409
+ lux.text(text)
410
+ }
411
+ }
412
+ }
413
+ if(token.kind != LEnd) {
414
+ let css = token.kind.{
415
+ | LChar => Styles.codeStringCss
416
+ | LFloat => Styles.codeNumberCss
417
+ | LInt => Styles.codeNumberCss
418
+ | LKeyword => Styles.codeKeywordCss
419
+ | LNamespace => Styles.codeTypeCss
420
+ | LString => Styles.codeStringCss
421
+ | LWildcard => Styles.codeVariableCss
422
+ | LUpper {tokens.get(index - 1).any {_.kind == LArrowThin}} => Styles.codeStringCss
423
+ | LUpper => Styles.codeTypeCss
424
+ | LLower {tokens.get(index - 1).any {_.kind == LArrowThin}} => Styles.codeStringCss
425
+ | LLower {tokens.get(index - 1).any {t =>
426
+ t.kind == LBracketRight || t.kind == LDot
427
+ }} => Styles.codeCallCss
428
+ | LLower {tokens.grab(index + 1).kind == LBracketLeft} => Styles.codeCallCss
429
+ | LLower => Styles.codeVariableCss
430
+ | _ => Styles.codeOtherCss
431
+ }
432
+ if(css == Styles.codeOtherCss) {
433
+ lux.text(code.slice(token.startOffset, token.stopOffset))
434
+ } else {
435
+ lux.span {
436
+ lux.cssClass(css)
437
+ lux.text(code.slice(token.startOffset, token.stopOffset))
438
+ }
439
+ }
440
+ offset = token.stopOffset
441
+ }
442
+ index += 1
443
+ }
444
+ }
445
+ }.else {lux.text(code)}
446
+ }
447
+
448
+ kebabCase(text: String): String {
449
+ mutable result = text.trim().lower().filter {c => c.isAsciiLetterOrDigit() || c == ' '}
450
+ doWhile {
451
+ let before = result
452
+ result = result.replace(" ", "")
453
+ result != before
454
+ }
455
+ result.replace(" ", "-")
456
+ }