~repos /gromer

#golang#htmx#ssr

git clone https://pyrossh.dev/repos/gromer.git

gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.


160a5bdd pyros2097

5 years ago
improve code organisation
Files changed (22) hide show
  1. LICENSE +22 -0
  2. app_nowasm.go +4 -4
  3. attributes.go +2 -8
  4. attributes_test.go +2 -1
  5. component.go +2 -27
  6. concurrency.go +0 -15
  7. condition.go +0 -117
  8. context.go +0 -20
  9. element.go +121 -13
  10. element_test.go +100 -0
  11. html.go +0 -5
  12. js_nowasm.go +32 -41
  13. makefile +0 -31
  14. range_test.go +0 -91
  15. resource.go +0 -151
  16. resource_test.go +0 -85
  17. range.go → selectors.go +106 -0
  18. condition_test.go → selectors_test.go +90 -0
  19. strings.go +0 -45
  20. text.go +0 -120
  21. text_test.go +0 -101
  22. utils.go +56 -0
LICENSE CHANGED
@@ -19,3 +19,25 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
+
23
+ MIT License
24
+
25
+ Copyright (c) 2020 pyros2097
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to deal
29
+ in the Software without restriction, including without limitation the rights
30
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31
+ copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in all
35
+ copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43
+ SOFTWARE.
app_nowasm.go CHANGED
@@ -16,17 +16,17 @@ func getenv(k string) string {
16
16
  }
17
17
 
18
18
  func keepBodyClean() func() {
19
- panic(errNoWasm)
19
+ panic("wasm required")
20
20
  }
21
21
 
22
22
  func navigate(u *url.URL, updateHistory bool) error {
23
- panic(errNoWasm)
23
+ panic("wasm required")
24
24
  }
25
25
 
26
26
  func reload() {
27
- panic(errNoWasm)
27
+ panic("wasm required")
28
28
  }
29
29
 
30
30
  func run(r RenderFunc) {
31
- panic(errNoWasm)
31
+ panic("wasm required")
32
32
  }
attributes.go CHANGED
@@ -2,8 +2,6 @@ package app
2
2
 
3
3
  import (
4
4
  "context"
5
-
6
- "github.com/pyros2097/wapp/errors"
7
5
  )
8
6
 
9
7
  type baseAttribute struct {
@@ -57,18 +55,14 @@ func (c baseAttribute) children() []UI {
57
55
  }
58
56
 
59
57
  func (c baseAttribute) mount() error {
60
- return errors.New("condition is not mountable").
58
+ panic("cant mount attributes")
61
- Tag("name", c.name()).
62
- Tag("kind", c.Kind())
63
59
  }
64
60
 
65
61
  func (c baseAttribute) dismount() {
66
62
  }
67
63
 
68
64
  func (c baseAttribute) update(UI) error {
69
- return errors.New("condition cannot be updated").
65
+ panic("cant update attributes")
70
- Tag("name", c.name()).
71
- Tag("kind", c.Kind())
72
66
  }
73
67
 
74
68
  type CssAttribute struct {
attributes_test.go CHANGED
@@ -2,6 +2,7 @@ package app
2
2
 
3
3
  import (
4
4
  "bytes"
5
+ "strconv"
5
6
  "testing"
6
7
 
7
8
  "github.com/stretchr/testify/assert"
@@ -18,7 +19,7 @@ func Counter(c *RenderContext) UI {
18
19
  Text("-"),
19
20
  ),
20
21
  Div(
21
- Text(count()),
22
+ Text(strconv.Itoa(count())),
22
23
  ),
23
24
  Div(
24
25
  Text("+"),
component.go CHANGED
@@ -8,31 +8,6 @@ import (
8
8
  "github.com/pyros2097/wapp/errors"
9
9
  )
10
10
 
11
- // Composer is the interface that describes a customized, independent and
12
- // reusable UI element.
13
- //
14
- // Satisfying this interface is done by embedding app.Compo into a struct and
15
- // implementing the Render function.
16
- //
17
- // Example:
18
- // type Hello struct {
19
- // app.Compo
20
- // }
21
- //
22
- // func (c *Hello) Render() app.UI {
23
- // return app.Text("hello")
24
- // }
25
- type Composer interface {
26
- UI
27
-
28
- // Render returns the node tree that define how the component is desplayed.
29
- Render() UI
30
-
31
- // Update update the component appearance. It should be called when a field
32
- // used to render the component has been modified.
33
- Update()
34
- }
35
-
36
11
  var contextMap = map[int]*RenderContext{}
37
12
  var contextIndex = 0
38
13
 
@@ -95,7 +70,7 @@ func (r RenderFunc) setSelf(n UI) {
95
70
  if n != nil {
96
71
  println("new context")
97
72
  c := NewRenderContext()
98
- c.this = n.(Composer)
73
+ c.this = n.(RenderFunc)
99
74
  return
100
75
  }
101
76
 
@@ -268,7 +243,7 @@ type RenderContext struct {
268
243
  contextMapIndex int
269
244
  parentElem UI
270
245
  root UI
271
- this Composer
246
+ this RenderFunc
272
247
  index int
273
248
  values map[int]interface{}
274
249
  eindex int
concurrency.go DELETED
@@ -1,15 +0,0 @@
1
- package app
2
-
3
- var (
4
- dispatch Dispatcher = Dispatch
5
- uiChan = make(chan func(), 512)
6
- )
7
-
8
- // Dispatcher is a function that executes the given function on the goroutine
9
- // dedicated to UI.
10
- type Dispatcher func(func())
11
-
12
- // Dispatch executes the given function on the UI goroutine.
13
- func Dispatch(f func()) {
14
- uiChan <- f
15
- }
condition.go DELETED
@@ -1,117 +0,0 @@
1
- package app
2
-
3
- import (
4
- "context"
5
-
6
- "github.com/pyros2097/wapp/errors"
7
- )
8
-
9
- // Condition represents a control structure that displays nodes depending on a
10
- // given expression.
11
- type Condition interface {
12
- UI
13
-
14
- // ElseIf sets the condition with the given nodes if previous expressions
15
- // were not met and given expression is true.
16
- ElseIf(expr bool, elems ...UI) Condition
17
-
18
- // Else sets the condition with the given UI elements if previous
19
- // expressions were not met.
20
- Else(elems ...UI) Condition
21
- }
22
-
23
- // If returns a condition that filters the given elements according to the given
24
- // expression.
25
- func If(expr bool, elems ...UI) Condition {
26
- if !expr {
27
- elems = nil
28
- }
29
-
30
- return condition{
31
- body: FilterUIElems(elems...),
32
- satisfied: expr,
33
- }
34
- }
35
-
36
- type condition struct {
37
- body []UI
38
- satisfied bool
39
- }
40
-
41
- func (c condition) ElseIf(expr bool, elems ...UI) Condition {
42
- if c.satisfied {
43
- return c
44
- }
45
-
46
- if expr {
47
- c.body = FilterUIElems(elems...)
48
- c.satisfied = expr
49
- }
50
-
51
- return c
52
- }
53
-
54
- func (c condition) Else(elems ...UI) Condition {
55
- return c.ElseIf(true, elems...)
56
- }
57
-
58
- func (c condition) Kind() Kind {
59
- return Selector
60
- }
61
-
62
- func (c condition) JSValue() Value {
63
- return nil
64
- }
65
-
66
- func (c condition) Mounted() bool {
67
- return false
68
- }
69
-
70
- func (c condition) name() string {
71
- return "if.else"
72
- }
73
-
74
- func (c condition) self() UI {
75
- return c
76
- }
77
-
78
- func (c condition) setSelf(UI) {
79
- }
80
-
81
- func (c condition) context() context.Context {
82
- return nil
83
- }
84
-
85
- func (c condition) attributes() map[string]string {
86
- return nil
87
- }
88
-
89
- func (c condition) eventHandlers() map[string]eventHandler {
90
- return nil
91
- }
92
-
93
- func (c condition) parent() UI {
94
- return nil
95
- }
96
-
97
- func (c condition) setParent(UI) {
98
- }
99
-
100
- func (c condition) children() []UI {
101
- return c.body
102
- }
103
-
104
- func (c condition) mount() error {
105
- return errors.New("condition is not mountable").
106
- Tag("name", c.name()).
107
- Tag("kind", c.Kind())
108
- }
109
-
110
- func (c condition) dismount() {
111
- }
112
-
113
- func (c condition) update(UI) error {
114
- return errors.New("condition cannot be updated").
115
- Tag("name", c.name()).
116
- Tag("kind", c.Kind())
117
- }
context.go DELETED
@@ -1,20 +0,0 @@
1
- package app
2
-
3
- import "context"
4
-
5
- // Context represents a context that is tied to a UI element. It is canceled
6
- // when the element is dismounted.
7
- //
8
- // It implements the context.Context interface.
9
- // https://golang.org/pkg/context/#Context
10
- type Context struct {
11
- context.Context
12
-
13
- // The UI element tied to the context.
14
- Src UI
15
-
16
- // The JavaScript value of the element tied to the context. This is a
17
- // shorthand for:
18
- // ctx.Src.JSValue()
19
- JSSrc Value
20
- }
element.go CHANGED
@@ -278,14 +278,14 @@ func (e *elem) updateAttrs(attrs map[string]string) {
278
278
  }
279
279
  }
280
280
 
281
- func (e *elem) setAttr(k string, v interface{}) {
281
+ func (e *elem) setAttr(k string, v string) {
282
282
  if e.attrs == nil {
283
283
  e.attrs = make(map[string]string)
284
284
  }
285
285
 
286
286
  switch k {
287
287
  case "style", "allow":
288
- s := e.attrs[k] + toString(v) + ";"
288
+ s := e.attrs[k] + v + ";"
289
289
  e.attrs[k] = s
290
290
  return
291
291
 
@@ -294,21 +294,17 @@ func (e *elem) setAttr(k string, v interface{}) {
294
294
  if s != "" {
295
295
  s += " "
296
296
  }
297
- s += toString(v)
297
+ s += v
298
298
  e.attrs[k] = s
299
299
  return
300
300
  }
301
-
302
- switch v := v.(type) {
301
+ if v == "false" {
303
- case bool:
304
- if !v {
305
- delete(e.attrs, k)
302
+ delete(e.attrs, k)
306
- return
303
+ return
307
- }
304
+ } else if v == "true" {
308
305
  e.attrs[k] = ""
309
-
310
- default:
306
+ } else {
311
- e.attrs[k] = toString(v)
307
+ e.attrs[k] = v
312
308
  }
313
309
  }
314
310
 
@@ -454,3 +450,115 @@ func (e *elem) OnInput(h EventHandler) *elem {
454
450
  e.setEventHandler("input", h)
455
451
  return e
456
452
  }
453
+
454
+ type text struct {
455
+ jsvalue Value
456
+ parentElem UI
457
+ value string
458
+ }
459
+
460
+ // Text creates a simple text element.
461
+ func Text(v string) UI {
462
+ return &text{value: v}
463
+ }
464
+
465
+ func (t *text) Kind() Kind {
466
+ return SimpleText
467
+ }
468
+
469
+ func (t *text) JSValue() Value {
470
+ return t.jsvalue
471
+ }
472
+
473
+ func (t *text) Mounted() bool {
474
+ return t.jsvalue != nil
475
+ }
476
+
477
+ func (t *text) name() string {
478
+ return "text"
479
+ }
480
+
481
+ func (t *text) self() UI {
482
+ return t
483
+ }
484
+
485
+ func (t *text) setSelf(n UI) {
486
+ }
487
+
488
+ func (t *text) context() context.Context {
489
+ return context.TODO()
490
+ }
491
+
492
+ func (t *text) attributes() map[string]string {
493
+ return nil
494
+ }
495
+
496
+ func (t *text) eventHandlers() map[string]eventHandler {
497
+ return nil
498
+ }
499
+
500
+ func (t *text) parent() UI {
501
+ return t.parentElem
502
+ }
503
+
504
+ func (t *text) setParent(p UI) {
505
+ t.parentElem = p
506
+ }
507
+
508
+ func (t *text) children() []UI {
509
+ return nil
510
+ }
511
+
512
+ func (t *text) mount() error {
513
+ if t.Mounted() {
514
+ return errors.New("mounting ui element failed").
515
+ Tag("reason", "already mounted").
516
+ Tag("kind", t.Kind()).
517
+ Tag("name", t.name()).
518
+ Tag("value", t.value)
519
+ }
520
+
521
+ t.jsvalue = Window().
522
+ Get("document").
523
+ Call("createTextNode", t.value)
524
+
525
+ return nil
526
+ }
527
+
528
+ func (t *text) dismount() {
529
+ t.jsvalue = nil
530
+ }
531
+
532
+ func (t *text) update(n UI) error {
533
+ if !t.Mounted() {
534
+ return nil
535
+ }
536
+
537
+ o, isText := n.(*text)
538
+ if !isText {
539
+ return errors.New("updating ui element failed").
540
+ Tag("replace", true).
541
+ Tag("reason", "different element types").
542
+ Tag("current-kind", t.Kind()).
543
+ Tag("current-name", t.name()).
544
+ Tag("updated-kind", n.Kind()).
545
+ Tag("updated-name", n.name())
546
+ }
547
+
548
+ if t.value != o.value {
549
+ t.value = o.value
550
+ t.jsvalue.Set("nodeValue", o.value)
551
+ }
552
+
553
+ return nil
554
+ }
555
+
556
+ func (t *text) Html(w io.Writer) {
557
+ t.HtmlWithIndent(w, 0)
558
+ }
559
+
560
+ func (t *text) HtmlWithIndent(w io.Writer, indent int) {
561
+ writeIndent(w, indent)
562
+ // html.EscapeString(
563
+ w.Write(stob(t.value))
564
+ }
element_test.go CHANGED
@@ -359,3 +359,103 @@ package app
359
359
  // // },
360
360
  // // })
361
361
  // // }
362
+
363
+ // func TestTextMountDismout(t *testing.T) {
364
+ // testMountDismount(t, []mountTest{
365
+ // {
366
+ // scenario: "text",
367
+ // node: Text("hello"),
368
+ // },
369
+ // })
370
+ // }
371
+
372
+ // func TestTextUpdate(t *testing.T) {
373
+ // testUpdate(t, []updateTest{
374
+ // {
375
+ // scenario: "text element returns replace error when updated with a non text-element",
376
+ // a: Text("hello"),
377
+ // b: Div(),
378
+ // replaceErr: true,
379
+ // },
380
+ // {
381
+ // scenario: "text element is updated",
382
+ // a: Text("hello"),
383
+ // b: Text("world"),
384
+ // matches: []TestUIDescriptor{
385
+ // {
386
+ // Expected: Text("world"),
387
+ // },
388
+ // },
389
+ // },
390
+
391
+ // {
392
+ // scenario: "text is replaced by a html elem",
393
+ // a: Div().Body(
394
+ // Text("hello"),
395
+ // ),
396
+ // b: Div().Body(
397
+ // H2().Text("hello"),
398
+ // ),
399
+ // matches: []TestUIDescriptor{
400
+ // {
401
+ // Path: TestPath(),
402
+ // Expected: Div(),
403
+ // },
404
+ // {
405
+ // Path: TestPath(0),
406
+ // Expected: H2(),
407
+ // },
408
+ // {
409
+ // Path: TestPath(0, 0),
410
+ // Expected: Text("hello"),
411
+ // },
412
+ // },
413
+ // },
414
+ // {
415
+ // scenario: "text is replaced by a component",
416
+ // a: Div().Body(
417
+ // Text("hello"),
418
+ // ),
419
+ // // b: Div().Body(
420
+ // // &hello{},
421
+ // // ),
422
+ // matches: []TestUIDescriptor{
423
+ // {
424
+ // Path: TestPath(),
425
+ // Expected: Div(),
426
+ // },
427
+ // // {
428
+ // // Path: TestPath(0),
429
+ // // Expected: &hello{},
430
+ // // },
431
+ // {
432
+ // Path: TestPath(0, 0, 0),
433
+ // Expected: H1(),
434
+ // },
435
+ // {
436
+ // Path: TestPath(0, 0, 0, 0),
437
+ // Expected: Text("hello, "),
438
+ // },
439
+ // },
440
+ // },
441
+ // {
442
+ // scenario: "text is replaced by a raw html element",
443
+ // a: Div().Body(
444
+ // Text("hello"),
445
+ // ),
446
+ // b: Div().Body(
447
+ // Raw("<svg></svg>"),
448
+ // ),
449
+ // matches: []TestUIDescriptor{
450
+ // {
451
+ // Path: TestPath(),
452
+ // Expected: Div(),
453
+ // },
454
+ // {
455
+ // Path: TestPath(0),
456
+ // Expected: Raw("<svg></svg>"),
457
+ // },
458
+ // },
459
+ // },
460
+ // })
461
+ // }
html.go CHANGED
@@ -58,11 +58,6 @@ func Script(str string) *elem {
58
58
  // return e
59
59
  // }
60
60
 
61
- // func (e *elem) OnClick(h EventHandler) *elem {
62
- // e.setEventHandler("click", h)
63
- // return e
64
- // }
65
-
66
61
  // func (e *elem) OnFocus(h EventHandler) *elem {
67
62
  // e.setEventHandler("focus", h)
68
63
  // return e
js_nowasm.go CHANGED
@@ -4,109 +4,100 @@ package app
4
4
 
5
5
  import (
6
6
  "net/url"
7
- "runtime"
8
-
9
- "github.com/pyros2097/wapp/errors"
10
- )
11
-
12
- var (
13
- errNoWasm = errors.New("unsupported instruction").
14
- Tag("required-architecture", "wasm").
15
- Tag("current-architecture", runtime.GOARCH)
16
7
  )
17
8
 
18
9
  type value struct{}
19
10
 
20
11
  func (v value) Bool() bool {
21
- panic(errNoWasm)
12
+ panic("wasm required")
22
13
  }
23
14
 
24
15
  func (v value) Call(m string, args ...interface{}) Value {
25
- panic(errNoWasm)
16
+ panic("wasm required")
26
17
  }
27
18
 
28
19
  func (v value) Float() float64 {
29
- panic(errNoWasm)
20
+ panic("wasm required")
30
21
  }
31
22
 
32
23
  func (v value) Get(p string) Value {
33
- panic(errNoWasm)
24
+ panic("wasm required")
34
25
  }
35
26
 
36
27
  func (v value) Index(i int) Value {
37
- panic(errNoWasm)
28
+ panic("wasm required")
38
29
  }
39
30
 
40
31
  func (v value) InstanceOf(t Value) bool {
41
- panic(errNoWasm)
32
+ panic("wasm required")
42
33
  }
43
34
 
44
35
  func (v value) Int() int {
45
- panic(errNoWasm)
36
+ panic("wasm required")
46
37
  }
47
38
 
48
39
  func (v value) Invoke(args ...interface{}) Value {
49
- panic(errNoWasm)
40
+ panic("wasm required")
50
41
  }
51
42
 
52
43
  func (v value) IsNaN() bool {
53
- panic(errNoWasm)
44
+ panic("wasm required")
54
45
  }
55
46
 
56
47
  func (v value) IsNull() bool {
57
- panic(errNoWasm)
48
+ panic("wasm required")
58
49
  }
59
50
 
60
51
  func (v value) IsUndefined() bool {
61
- panic(errNoWasm)
52
+ panic("wasm required")
62
53
  }
63
54
 
64
55
  func (v value) JSValue() Value {
65
- panic(errNoWasm)
56
+ panic("wasm required")
66
57
  }
67
58
 
68
59
  func (v value) Length() int {
69
- panic(errNoWasm)
60
+ panic("wasm required")
70
61
  }
71
62
 
72
63
  func (v value) New(args ...interface{}) Value {
73
- panic(errNoWasm)
64
+ panic("wasm required")
74
65
  }
75
66
 
76
67
  func (v value) Set(p string, x interface{}) {
77
- panic(errNoWasm)
68
+ panic("wasm required")
78
69
  }
79
70
 
80
71
  func (v value) SetIndex(i int, x interface{}) {
81
- panic(errNoWasm)
72
+ panic("wasm required")
82
73
  }
83
74
 
84
75
  func (v value) String() string {
85
- panic(errNoWasm)
76
+ panic("wasm required")
86
77
  }
87
78
 
88
79
  func (v value) Truthy() bool {
89
- panic(errNoWasm)
80
+ panic("wasm required")
90
81
  }
91
82
 
92
83
  func (v value) Type() Type {
93
- panic(errNoWasm)
84
+ panic("wasm required")
94
85
  }
95
86
 
96
87
  func null() Value {
97
- panic(errNoWasm)
88
+ panic("wasm required")
98
89
  }
99
90
 
100
91
  func undefined() Value {
101
- panic(errNoWasm)
92
+ panic("wasm required")
102
93
  }
103
94
 
104
95
  func valueOf(x interface{}) Value {
105
- panic(errNoWasm)
96
+ panic("wasm required")
106
97
  }
107
98
 
108
99
  func funcOf(fn func(this Value, args []Value) interface{}) Func {
109
- panic(errNoWasm)
100
+ panic("wasm required")
110
101
  }
111
102
 
112
103
  type browserWindow struct {
@@ -114,37 +105,37 @@ type browserWindow struct {
114
105
  }
115
106
 
116
107
  func (w browserWindow) URL() *url.URL {
117
- panic(errNoWasm)
108
+ panic("wasm required")
118
109
  }
119
110
 
120
111
  func (w browserWindow) Size() (width, height int) {
121
- panic(errNoWasm)
112
+ panic("wasm required")
122
113
  }
123
114
 
124
115
  func (w browserWindow) CursorPosition() (x, y int) {
125
- panic(errNoWasm)
116
+ panic("wasm required")
126
117
  }
127
118
 
128
119
  func (w browserWindow) setCursorPosition(x, y int) {
129
- panic(errNoWasm)
120
+ panic("wasm required")
130
121
  }
131
122
 
132
123
  func (w *browserWindow) GetElementByID(id string) Value {
133
- panic(errNoWasm)
124
+ panic("wasm required")
134
125
  }
135
126
 
136
127
  func (w *browserWindow) ScrollToID(id string) {
137
- panic(errNoWasm)
128
+ panic("wasm required")
138
129
  }
139
130
 
140
131
  func (w *browserWindow) AddEventListener(event string, h EventHandler) func() {
141
- panic(errNoWasm)
132
+ panic("wasm required")
142
133
  }
143
134
 
144
135
  func copyBytesToGo(dst []byte, src Value) int {
145
- panic(errNoWasm)
136
+ panic("wasm required")
146
137
  }
147
138
 
148
139
  func copyBytesToJS(dst Value, src []byte) int {
149
- panic(errNoWasm)
140
+ panic("wasm required")
150
141
  }
makefile CHANGED
@@ -11,34 +11,3 @@ test:
11
11
  go test -race ./...
12
12
  @echo "\033[94m\n• Running go wasm tests\033[00m"
13
13
  GOARCH=wasm GOOS=js go test ./pkg/app
14
-
15
- release: test
16
- ifdef VERSION
17
- @echo "\033[94m\n• Releasing ${VERSION}\033[00m"
18
- @git tag ${VERSION}
19
- @git push origin ${VERSION}
20
-
21
- else
22
- @echo "\033[94m\n• Releasing version\033[00m"
23
- @echo "\033[91mVERSION is not defided\033[00m"
24
- @echo "~> make VERSION=\033[90mv6.0.0\033[00m release"
25
- endif
26
-
27
-
28
- build:
29
- @echo "\033[94m• Building go-app documentation PWA\033[00m"
30
- @GOARCH=wasm GOOS=js go build -o docs/web/app.wasm ./docs/src
31
- @echo "\033[94m• Building go-app documentation\033[00m"
32
- @go build -o docs/documentation ./docs/src
33
-
34
- run: build
35
- @echo "\033[94m• Running go-app documentation server\033[00m"
36
- @cd docs && ./documentation local
37
-
38
- github: build
39
- @echo "\033[94m• Generating GitHub Pages\033[00m"
40
- @cd docs && ./documentation github
41
-
42
- clean:
43
- @go clean -v ./...
44
- -@rm docs/documentation
range_test.go DELETED
@@ -1,91 +0,0 @@
1
- package app
2
-
3
- // import "testing"
4
-
5
- // func TestRange(t *testing.T) {
6
- // testUpdate(t, []updateTest{
7
- // {
8
- // scenario: "range slice is updated",
9
- // a: Div().Body(
10
- // Range([]string{"hello", "world"}).Slice(func(i int) UI {
11
- // src := []string{"hello", "world"}
12
- // return Text(src[i])
13
- // }),
14
- // ),
15
- // b: Div().Body(
16
- // Range([]string{"hello", "maxoo"}).Slice(func(i int) UI {
17
- // src := []string{"hello", "maxoo"}
18
- // return Text(src[i])
19
- // }),
20
- // ),
21
- // matches: []TestUIDescriptor{
22
- // {
23
- // Path: TestPath(),
24
- // Expected: Div(),
25
- // },
26
- // {
27
- // Path: TestPath(0),
28
- // Expected: Text("hello"),
29
- // },
30
- // {
31
- // Path: TestPath(1),
32
- // Expected: Text("maxoo"),
33
- // },
34
- // },
35
- // },
36
- // {
37
- // scenario: "range slice is updated to be empty",
38
- // a: Div().Body(
39
- // Range([]string{"hello", "world"}).Slice(func(i int) UI {
40
- // src := []string{"hello", "world"}
41
- // return Text(src[i])
42
- // }),
43
- // ),
44
- // b: Div().Body(
45
- // Range([]string{}).Slice(func(i int) UI {
46
- // src := []string{"hello", "maxoo"}
47
- // return Text(src[i])
48
- // }),
49
- // ),
50
- // matches: []TestUIDescriptor{
51
- // {
52
- // Path: TestPath(),
53
- // Expected: Div(),
54
- // },
55
- // {
56
- // Path: TestPath(0),
57
- // Expected: nil,
58
- // },
59
- // {
60
- // Path: TestPath(1),
61
- // Expected: nil,
62
- // },
63
- // },
64
- // },
65
- // {
66
- // scenario: "range map is updated",
67
- // a: Div().Body(
68
- // Range(map[string]string{"key": "value"}).Map(func(k string) UI {
69
- // src := map[string]string{"key": "value"}
70
- // return Text(src[k])
71
- // }),
72
- // ),
73
- // b: Div().Body(
74
- // Range(map[string]string{"key": "value"}).Map(func(k string) UI {
75
- // src := map[string]string{"key": "maxoo"}
76
- // return Text(src[k])
77
- // }),
78
- // ),
79
- // matches: []TestUIDescriptor{
80
- // {
81
- // Path: TestPath(),
82
- // Expected: Div(),
83
- // },
84
- // {
85
- // Path: TestPath(0),
86
- // Expected: Text("maxoo"),
87
- // },
88
- // },
89
- // },
90
- // })
91
- // }
resource.go DELETED
@@ -1,151 +0,0 @@
1
- // +build !wasm
2
-
3
- package app
4
-
5
- import (
6
- "net/http"
7
- "strings"
8
- )
9
-
10
- // ResourceProvider is the interface that describes a provider for resources.
11
- //
12
- // App resources are the mandatory resources required to run a PWA. They are
13
- // generated by the Handler and are accessible from the root path. Eg:
14
- // "/app-worker.js"
15
- // "/manifest.json"
16
- // "/wasm_exec.js"
17
- //
18
- // Static resources are the resources used by the PWA such as the web assembly
19
- // binary, styles, scripts, or images. They can be located on a localhost or a
20
- // remote bucket. In order to avoid confusion with PWA required resources,
21
- // static resources URL paths are always prefixed by "/web". Eg:
22
- // "/web/app.wasm"
23
- // "/web/main.css"
24
- // "/web/background.jpg"
25
- //
26
- // If the resource provider is an http.Handler, the handler is used to serve
27
- // static resources requests.
28
- type ResourceProvider interface {
29
- // The path to the root directory where app resources are accessible.
30
- AppResources() string
31
-
32
- // The path or URL where the web directory that contains static resources is
33
- // located.
34
- StaticResources() string
35
-
36
- // The URL of the app.wasm file. This must match the pattern:
37
- // StaticResources/web/WASM_FILE.
38
- AppWASM() string
39
-
40
- // The URL of the robots.txt file. This must match the pattern:
41
- // StaticResources/web/robots.txt.
42
- RobotsTxt() string
43
-
44
- // The URL of the ads.txt file. This must match the pattern:
45
- // StaticResources/web/ads.txt.
46
- AdsTxt() string
47
- }
48
-
49
- // LocalDir returns a resource provider that serves static resources from a
50
- // local directory located at the given path.
51
- func LocalDir(path string) ResourceProvider {
52
- return localDir{
53
- Handler: http.StripPrefix("/web/", http.FileServer(http.Dir(path))),
54
- path: path,
55
- }
56
- }
57
-
58
- type localDir struct {
59
- http.Handler
60
- path string
61
- }
62
-
63
- func (d localDir) AppResources() string {
64
- return ""
65
- }
66
-
67
- func (d localDir) StaticResources() string {
68
- return ""
69
- }
70
-
71
- func (d localDir) AppWASM() string {
72
- return "/web/app.wasm"
73
- }
74
-
75
- func (d localDir) RobotsTxt() string {
76
- return "/web/robots.txt"
77
- }
78
-
79
- func (d localDir) AdsTxt() string {
80
- return "/web/ads.txt"
81
- }
82
-
83
- // RemoteBucket returns a resource provider that provides resources from a
84
- // remote bucket such as Amazon S3 or Google Cloud Storage.
85
- func RemoteBucket(url string) ResourceProvider {
86
- url = strings.TrimSuffix(url, "/")
87
- url = strings.TrimSuffix(url, "/web")
88
-
89
- return remoteBucket{
90
- url: url,
91
- }
92
- }
93
-
94
- type remoteBucket struct {
95
- url string
96
- }
97
-
98
- func (b remoteBucket) AppResources() string {
99
- return ""
100
- }
101
-
102
- func (b remoteBucket) StaticResources() string {
103
- return b.url
104
- }
105
-
106
- func (b remoteBucket) AppWASM() string {
107
- return b.StaticResources() + "/web/app.wasm"
108
- }
109
-
110
- func (b remoteBucket) RobotsTxt() string {
111
- return b.StaticResources() + "/web/robots.txt"
112
- }
113
-
114
- func (b remoteBucket) AdsTxt() string {
115
- return b.StaticResources() + "/web/ads.txt"
116
- }
117
-
118
- // GitHubPages returns a resource provider that provides resources from GitHub
119
- // pages. This provider must only be used to generate static websites with the
120
- // GenerateStaticWebsite function.
121
- func GitHubPages(repoName string) ResourceProvider {
122
- if !strings.HasPrefix(repoName, "/") {
123
- repoName = "/" + repoName
124
- }
125
-
126
- return gitHubPages{repo: repoName}
127
- }
128
-
129
- type gitHubPages struct {
130
- repo string
131
- }
132
-
133
- func (g gitHubPages) AppResources() string {
134
- return g.repo
135
- }
136
-
137
- func (g gitHubPages) StaticResources() string {
138
- return g.repo
139
- }
140
-
141
- func (g gitHubPages) AppWASM() string {
142
- return g.StaticResources() + "/web/app.wasm"
143
- }
144
-
145
- func (g gitHubPages) RobotsTxt() string {
146
- return g.StaticResources() + "/web/robots.txt"
147
- }
148
-
149
- func (g gitHubPages) AdsTxt() string {
150
- return g.StaticResources() + "/web/ads.txt"
151
- }
resource_test.go DELETED
@@ -1,85 +0,0 @@
1
- // +build !wasm
2
-
3
- package app
4
-
5
- import (
6
- "io/ioutil"
7
- "net/http"
8
- "net/http/httptest"
9
- "strings"
10
- "testing"
11
-
12
- "github.com/stretchr/testify/require"
13
- )
14
-
15
- func TestLocalDir(t *testing.T) {
16
- testSkipWasm(t)
17
-
18
- utests := []struct {
19
- scenario string
20
- provider ResourceProvider
21
- }{
22
- {
23
- scenario: "from web directory",
24
- provider: LocalDir("web"),
25
- },
26
- }
27
-
28
- for _, u := range utests {
29
- t.Run(u.scenario, func(t *testing.T) {
30
- h := u.provider.(localDir)
31
-
32
- require.Empty(t, h.StaticResources())
33
- require.Equal(t, "/web/app.wasm", h.AppWASM())
34
- require.Equal(t, "/web/robots.txt", h.RobotsTxt())
35
- require.Equal(t, "/web/ads.txt", h.AdsTxt())
36
-
37
- close := testCreateDir(t, "web")
38
- defer close()
39
-
40
- resources := []string{
41
- "/web/test",
42
- h.AppWASM(),
43
- h.RobotsTxt(),
44
- }
45
-
46
- for _, r := range resources {
47
- t.Run(r, func(t *testing.T) {
48
- path := strings.Replace(r, "/web", h.path, 1)
49
- err := ioutil.WriteFile(path, stob("hello"), 0666)
50
- require.NoError(t, err)
51
-
52
- req := httptest.NewRequest(http.MethodGet, r, nil)
53
- res := httptest.NewRecorder()
54
- h.ServeHTTP(res, req)
55
- require.Equal(t, "hello", res.Body.String())
56
- })
57
- }
58
- })
59
- }
60
- }
61
-
62
- func TestRemoteBucket(t *testing.T) {
63
- utests := []struct {
64
- scenario string
65
- provider ResourceProvider
66
- }{
67
- {
68
- scenario: "remote bucket",
69
- provider: RemoteBucket("https://storage.googleapis.com/test"),
70
- },
71
- {
72
- scenario: "remote bucket with web suffix",
73
- provider: RemoteBucket("https://storage.googleapis.com/test/web/"),
74
- },
75
- }
76
-
77
- for _, u := range utests {
78
- t.Run(u.scenario, func(t *testing.T) {
79
- require.Equal(t, "https://storage.googleapis.com/test", u.provider.StaticResources())
80
- require.Equal(t, "https://storage.googleapis.com/test/web/app.wasm", u.provider.AppWASM())
81
- require.Equal(t, "https://storage.googleapis.com/test/web/robots.txt", u.provider.RobotsTxt())
82
- require.Equal(t, "https://storage.googleapis.com/test/web/ads.txt", u.provider.AdsTxt())
83
- })
84
- }
85
- }
range.go → selectors.go RENAMED
@@ -149,3 +149,109 @@ func (r rangeLoop) update(UI) error {
149
149
 
150
150
  func (r rangeLoop) onNav(*url.URL) {
151
151
  }
152
+
153
+ // Condition represents a control structure that displays nodes depending on a
154
+ // given expression.
155
+ type Condition interface {
156
+ UI
157
+
158
+ // ElseIf sets the condition with the given nodes if previous expressions
159
+ // were not met and given expression is true.
160
+ ElseIf(expr bool, elems ...UI) Condition
161
+
162
+ // Else sets the condition with the given UI elements if previous
163
+ // expressions were not met.
164
+ Else(elems ...UI) Condition
165
+ }
166
+
167
+ // If returns a condition that filters the given elements according to the given
168
+ // expression.
169
+ func If(expr bool, elems ...UI) Condition {
170
+ if !expr {
171
+ elems = nil
172
+ }
173
+
174
+ return condition{
175
+ body: FilterUIElems(elems...),
176
+ satisfied: expr,
177
+ }
178
+ }
179
+
180
+ type condition struct {
181
+ body []UI
182
+ satisfied bool
183
+ }
184
+
185
+ func (c condition) ElseIf(expr bool, elems ...UI) Condition {
186
+ if c.satisfied {
187
+ return c
188
+ }
189
+
190
+ if expr {
191
+ c.body = FilterUIElems(elems...)
192
+ c.satisfied = expr
193
+ }
194
+
195
+ return c
196
+ }
197
+
198
+ func (c condition) Else(elems ...UI) Condition {
199
+ return c.ElseIf(true, elems...)
200
+ }
201
+
202
+ func (c condition) Kind() Kind {
203
+ return Selector
204
+ }
205
+
206
+ func (c condition) JSValue() Value {
207
+ return nil
208
+ }
209
+
210
+ func (c condition) Mounted() bool {
211
+ return false
212
+ }
213
+
214
+ func (c condition) name() string {
215
+ return "if.else"
216
+ }
217
+
218
+ func (c condition) self() UI {
219
+ return c
220
+ }
221
+
222
+ func (c condition) setSelf(UI) {
223
+ }
224
+
225
+ func (c condition) context() context.Context {
226
+ return nil
227
+ }
228
+
229
+ func (c condition) attributes() map[string]string {
230
+ return nil
231
+ }
232
+
233
+ func (c condition) eventHandlers() map[string]eventHandler {
234
+ return nil
235
+ }
236
+
237
+ func (c condition) parent() UI {
238
+ return nil
239
+ }
240
+
241
+ func (c condition) setParent(UI) {
242
+ }
243
+
244
+ func (c condition) children() []UI {
245
+ return c.body
246
+ }
247
+
248
+ func (c condition) mount() error {
249
+ panic("can't mout condition")
250
+ }
251
+
252
+ func (c condition) dismount() {
253
+ }
254
+
255
+ func (c condition) update(UI) error {
256
+ panic("condition cannot be updated")
257
+ }
condition_test.go → selectors_test.go RENAMED
@@ -1,5 +1,95 @@
1
1
  package app
2
2
 
3
+ // import "testing"
4
+
5
+ // func TestRange(t *testing.T) {
6
+ // testUpdate(t, []updateTest{
7
+ // {
8
+ // scenario: "range slice is updated",
9
+ // a: Div().Body(
10
+ // Range([]string{"hello", "world"}).Slice(func(i int) UI {
11
+ // src := []string{"hello", "world"}
12
+ // return Text(src[i])
13
+ // }),
14
+ // ),
15
+ // b: Div().Body(
16
+ // Range([]string{"hello", "maxoo"}).Slice(func(i int) UI {
17
+ // src := []string{"hello", "maxoo"}
18
+ // return Text(src[i])
19
+ // }),
20
+ // ),
21
+ // matches: []TestUIDescriptor{
22
+ // {
23
+ // Path: TestPath(),
24
+ // Expected: Div(),
25
+ // },
26
+ // {
27
+ // Path: TestPath(0),
28
+ // Expected: Text("hello"),
29
+ // },
30
+ // {
31
+ // Path: TestPath(1),
32
+ // Expected: Text("maxoo"),
33
+ // },
34
+ // },
35
+ // },
36
+ // {
37
+ // scenario: "range slice is updated to be empty",
38
+ // a: Div().Body(
39
+ // Range([]string{"hello", "world"}).Slice(func(i int) UI {
40
+ // src := []string{"hello", "world"}
41
+ // return Text(src[i])
42
+ // }),
43
+ // ),
44
+ // b: Div().Body(
45
+ // Range([]string{}).Slice(func(i int) UI {
46
+ // src := []string{"hello", "maxoo"}
47
+ // return Text(src[i])
48
+ // }),
49
+ // ),
50
+ // matches: []TestUIDescriptor{
51
+ // {
52
+ // Path: TestPath(),
53
+ // Expected: Div(),
54
+ // },
55
+ // {
56
+ // Path: TestPath(0),
57
+ // Expected: nil,
58
+ // },
59
+ // {
60
+ // Path: TestPath(1),
61
+ // Expected: nil,
62
+ // },
63
+ // },
64
+ // },
65
+ // {
66
+ // scenario: "range map is updated",
67
+ // a: Div().Body(
68
+ // Range(map[string]string{"key": "value"}).Map(func(k string) UI {
69
+ // src := map[string]string{"key": "value"}
70
+ // return Text(src[k])
71
+ // }),
72
+ // ),
73
+ // b: Div().Body(
74
+ // Range(map[string]string{"key": "value"}).Map(func(k string) UI {
75
+ // src := map[string]string{"key": "maxoo"}
76
+ // return Text(src[k])
77
+ // }),
78
+ // ),
79
+ // matches: []TestUIDescriptor{
80
+ // {
81
+ // Path: TestPath(),
82
+ // Expected: Div(),
83
+ // },
84
+ // {
85
+ // Path: TestPath(0),
86
+ // Expected: Text("maxoo"),
87
+ // },
88
+ // },
89
+ // },
90
+ // })
91
+ // }
92
+
3
93
  // func TestCondition(t *testing.T) {
4
94
  // testUpdate(t, []updateTest{
5
95
  // {
strings.go DELETED
@@ -1,45 +0,0 @@
1
- package app
2
-
3
- import (
4
- "fmt"
5
- "io"
6
- "strconv"
7
- "unsafe"
8
- )
9
-
10
- func toString(v interface{}) string {
11
- switch v := v.(type) {
12
- case string:
13
- return v
14
-
15
- case []byte:
16
- return btos(v)
17
-
18
- case int:
19
- return strconv.Itoa(v)
20
-
21
- case float64:
22
- return strconv.FormatFloat(v, 'f', 4, 64)
23
-
24
- default:
25
- return fmt.Sprint(v)
26
- }
27
- }
28
-
29
- func writeIndent(w io.Writer, indent int) {
30
- for i := 0; i < indent*4; i++ {
31
- w.Write(stob(" "))
32
- }
33
- }
34
-
35
- func ln() []byte {
36
- return stob("\n")
37
- }
38
-
39
- func btos(b []byte) string {
40
- return *(*string)(unsafe.Pointer(&b))
41
- }
42
-
43
- func stob(s string) []byte {
44
- return *(*[]byte)(unsafe.Pointer(&s))
45
- }
text.go DELETED
@@ -1,120 +0,0 @@
1
- package app
2
-
3
- import (
4
- "context"
5
- "io"
6
-
7
- "github.com/pyros2097/wapp/errors"
8
- )
9
-
10
- // Text creates a simple text element.
11
- func Text(v interface{}) UI {
12
- return &text{value: toString(v)}
13
- }
14
-
15
- type text struct {
16
- jsvalue Value
17
- parentElem UI
18
- value string
19
- }
20
-
21
- func (t *text) Kind() Kind {
22
- return SimpleText
23
- }
24
-
25
- func (t *text) JSValue() Value {
26
- return t.jsvalue
27
- }
28
-
29
- func (t *text) Mounted() bool {
30
- return t.jsvalue != nil
31
- }
32
-
33
- func (t *text) name() string {
34
- return "text"
35
- }
36
-
37
- func (t *text) self() UI {
38
- return t
39
- }
40
-
41
- func (t *text) setSelf(n UI) {
42
- }
43
-
44
- func (t *text) context() context.Context {
45
- return context.TODO()
46
- }
47
-
48
- func (t *text) attributes() map[string]string {
49
- return nil
50
- }
51
-
52
- func (t *text) eventHandlers() map[string]eventHandler {
53
- return nil
54
- }
55
-
56
- func (t *text) parent() UI {
57
- return t.parentElem
58
- }
59
-
60
- func (t *text) setParent(p UI) {
61
- t.parentElem = p
62
- }
63
-
64
- func (t *text) children() []UI {
65
- return nil
66
- }
67
-
68
- func (t *text) mount() error {
69
- if t.Mounted() {
70
- return errors.New("mounting ui element failed").
71
- Tag("reason", "already mounted").
72
- Tag("kind", t.Kind()).
73
- Tag("name", t.name()).
74
- Tag("value", t.value)
75
- }
76
-
77
- t.jsvalue = Window().
78
- Get("document").
79
- Call("createTextNode", t.value)
80
-
81
- return nil
82
- }
83
-
84
- func (t *text) dismount() {
85
- t.jsvalue = nil
86
- }
87
-
88
- func (t *text) update(n UI) error {
89
- if !t.Mounted() {
90
- return nil
91
- }
92
-
93
- o, isText := n.(*text)
94
- if !isText {
95
- return errors.New("updating ui element failed").
96
- Tag("replace", true).
97
- Tag("reason", "different element types").
98
- Tag("current-kind", t.Kind()).
99
- Tag("current-name", t.name()).
100
- Tag("updated-kind", n.Kind()).
101
- Tag("updated-name", n.name())
102
- }
103
-
104
- if t.value != o.value {
105
- t.value = o.value
106
- t.jsvalue.Set("nodeValue", o.value)
107
- }
108
-
109
- return nil
110
- }
111
-
112
- func (t *text) Html(w io.Writer) {
113
- t.HtmlWithIndent(w, 0)
114
- }
115
-
116
- func (t *text) HtmlWithIndent(w io.Writer, indent int) {
117
- writeIndent(w, indent)
118
- // html.EscapeString(
119
- w.Write(stob(t.value))
120
- }
text_test.go DELETED
@@ -1,101 +0,0 @@
1
- package app
2
-
3
- // func TestTextMountDismout(t *testing.T) {
4
- // testMountDismount(t, []mountTest{
5
- // {
6
- // scenario: "text",
7
- // node: Text("hello"),
8
- // },
9
- // })
10
- // }
11
-
12
- // func TestTextUpdate(t *testing.T) {
13
- // testUpdate(t, []updateTest{
14
- // {
15
- // scenario: "text element returns replace error when updated with a non text-element",
16
- // a: Text("hello"),
17
- // b: Div(),
18
- // replaceErr: true,
19
- // },
20
- // {
21
- // scenario: "text element is updated",
22
- // a: Text("hello"),
23
- // b: Text("world"),
24
- // matches: []TestUIDescriptor{
25
- // {
26
- // Expected: Text("world"),
27
- // },
28
- // },
29
- // },
30
-
31
- // {
32
- // scenario: "text is replaced by a html elem",
33
- // a: Div().Body(
34
- // Text("hello"),
35
- // ),
36
- // b: Div().Body(
37
- // H2().Text("hello"),
38
- // ),
39
- // matches: []TestUIDescriptor{
40
- // {
41
- // Path: TestPath(),
42
- // Expected: Div(),
43
- // },
44
- // {
45
- // Path: TestPath(0),
46
- // Expected: H2(),
47
- // },
48
- // {
49
- // Path: TestPath(0, 0),
50
- // Expected: Text("hello"),
51
- // },
52
- // },
53
- // },
54
- // {
55
- // scenario: "text is replaced by a component",
56
- // a: Div().Body(
57
- // Text("hello"),
58
- // ),
59
- // // b: Div().Body(
60
- // // &hello{},
61
- // // ),
62
- // matches: []TestUIDescriptor{
63
- // {
64
- // Path: TestPath(),
65
- // Expected: Div(),
66
- // },
67
- // // {
68
- // // Path: TestPath(0),
69
- // // Expected: &hello{},
70
- // // },
71
- // {
72
- // Path: TestPath(0, 0, 0),
73
- // Expected: H1(),
74
- // },
75
- // {
76
- // Path: TestPath(0, 0, 0, 0),
77
- // Expected: Text("hello, "),
78
- // },
79
- // },
80
- // },
81
- // {
82
- // scenario: "text is replaced by a raw html element",
83
- // a: Div().Body(
84
- // Text("hello"),
85
- // ),
86
- // b: Div().Body(
87
- // Raw("<svg></svg>"),
88
- // ),
89
- // matches: []TestUIDescriptor{
90
- // {
91
- // Path: TestPath(),
92
- // Expected: Div(),
93
- // },
94
- // {
95
- // Path: TestPath(0),
96
- // Expected: Raw("<svg></svg>"),
97
- // },
98
- // },
99
- // },
100
- // })
101
- // }
utils.go ADDED
@@ -0,0 +1,56 @@
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+ "io"
6
+ "unsafe"
7
+ )
8
+
9
+ var (
10
+ dispatch Dispatcher = Dispatch
11
+ uiChan = make(chan func(), 512)
12
+ )
13
+
14
+ // Context represents a context that is tied to a UI element. It is canceled
15
+ // when the element is dismounted.
16
+ //
17
+ // It implements the context.Context interface.
18
+ // https://golang.org/pkg/context/#Context
19
+ type Context struct {
20
+ context.Context
21
+
22
+ // The UI element tied to the context.
23
+ Src UI
24
+
25
+ // The JavaScript value of the element tied to the context. This is a
26
+ // shorthand for:
27
+ // ctx.Src.JSValue()
28
+ JSSrc Value
29
+ }
30
+
31
+ // Dispatcher is a function that executes the given function on the goroutine
32
+ // dedicated to UI.
33
+ type Dispatcher func(func())
34
+
35
+ // Dispatch executes the given function on the UI goroutine.
36
+ func Dispatch(f func()) {
37
+ uiChan <- f
38
+ }
39
+
40
+ func writeIndent(w io.Writer, indent int) {
41
+ for i := 0; i < indent*4; i++ {
42
+ w.Write(stob(" "))
43
+ }
44
+ }
45
+
46
+ func ln() []byte {
47
+ return stob("\n")
48
+ }
49
+
50
+ func btos(b []byte) string {
51
+ return *(*string)(unsafe.Pointer(&b))
52
+ }
53
+
54
+ func stob(s string) []byte {
55
+ return *(*[]byte)(unsafe.Pointer(&s))
56
+ }