~repos /gromer
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
- LICENSE +22 -0
- app_nowasm.go +4 -4
- attributes.go +2 -8
- attributes_test.go +2 -1
- component.go +2 -27
- concurrency.go +0 -15
- condition.go +0 -117
- context.go +0 -20
- element.go +121 -13
- element_test.go +100 -0
- html.go +0 -5
- js_nowasm.go +32 -41
- makefile +0 -31
- range_test.go +0 -91
- resource.go +0 -151
- resource_test.go +0 -85
- range.go → selectors.go +106 -0
- condition_test.go → selectors_test.go +90 -0
- strings.go +0 -45
- text.go +0 -120
- text_test.go +0 -101
- 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(
|
|
19
|
+
panic("wasm required")
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
func navigate(u *url.URL, updateHistory bool) error {
|
|
23
|
-
panic(
|
|
23
|
+
panic("wasm required")
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
func reload() {
|
|
27
|
-
panic(
|
|
27
|
+
panic("wasm required")
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
func run(r RenderFunc) {
|
|
31
|
-
panic(
|
|
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
|
-
|
|
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
|
-
|
|
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.(
|
|
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
|
|
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
|
|
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] +
|
|
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 +=
|
|
297
|
+
s += v
|
|
298
298
|
e.attrs[k] = s
|
|
299
299
|
return
|
|
300
300
|
}
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
if v == "false" {
|
|
303
|
-
case bool:
|
|
304
|
-
if !v {
|
|
305
|
-
|
|
302
|
+
delete(e.attrs, k)
|
|
306
|
-
|
|
303
|
+
return
|
|
307
|
-
|
|
304
|
+
} else if v == "true" {
|
|
308
305
|
e.attrs[k] = ""
|
|
309
|
-
|
|
310
|
-
|
|
306
|
+
} else {
|
|
311
|
-
e.attrs[k] =
|
|
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(
|
|
12
|
+
panic("wasm required")
|
|
22
13
|
}
|
|
23
14
|
|
|
24
15
|
func (v value) Call(m string, args ...interface{}) Value {
|
|
25
|
-
panic(
|
|
16
|
+
panic("wasm required")
|
|
26
17
|
}
|
|
27
18
|
|
|
28
19
|
func (v value) Float() float64 {
|
|
29
|
-
panic(
|
|
20
|
+
panic("wasm required")
|
|
30
21
|
}
|
|
31
22
|
|
|
32
23
|
func (v value) Get(p string) Value {
|
|
33
|
-
panic(
|
|
24
|
+
panic("wasm required")
|
|
34
25
|
}
|
|
35
26
|
|
|
36
27
|
func (v value) Index(i int) Value {
|
|
37
|
-
panic(
|
|
28
|
+
panic("wasm required")
|
|
38
29
|
}
|
|
39
30
|
|
|
40
31
|
func (v value) InstanceOf(t Value) bool {
|
|
41
|
-
panic(
|
|
32
|
+
panic("wasm required")
|
|
42
33
|
}
|
|
43
34
|
|
|
44
35
|
func (v value) Int() int {
|
|
45
|
-
panic(
|
|
36
|
+
panic("wasm required")
|
|
46
37
|
}
|
|
47
38
|
|
|
48
39
|
func (v value) Invoke(args ...interface{}) Value {
|
|
49
|
-
panic(
|
|
40
|
+
panic("wasm required")
|
|
50
41
|
}
|
|
51
42
|
|
|
52
43
|
func (v value) IsNaN() bool {
|
|
53
|
-
panic(
|
|
44
|
+
panic("wasm required")
|
|
54
45
|
}
|
|
55
46
|
|
|
56
47
|
func (v value) IsNull() bool {
|
|
57
|
-
panic(
|
|
48
|
+
panic("wasm required")
|
|
58
49
|
}
|
|
59
50
|
|
|
60
51
|
func (v value) IsUndefined() bool {
|
|
61
|
-
panic(
|
|
52
|
+
panic("wasm required")
|
|
62
53
|
}
|
|
63
54
|
|
|
64
55
|
func (v value) JSValue() Value {
|
|
65
|
-
panic(
|
|
56
|
+
panic("wasm required")
|
|
66
57
|
}
|
|
67
58
|
|
|
68
59
|
func (v value) Length() int {
|
|
69
|
-
panic(
|
|
60
|
+
panic("wasm required")
|
|
70
61
|
}
|
|
71
62
|
|
|
72
63
|
func (v value) New(args ...interface{}) Value {
|
|
73
|
-
panic(
|
|
64
|
+
panic("wasm required")
|
|
74
65
|
}
|
|
75
66
|
|
|
76
67
|
func (v value) Set(p string, x interface{}) {
|
|
77
|
-
panic(
|
|
68
|
+
panic("wasm required")
|
|
78
69
|
}
|
|
79
70
|
|
|
80
71
|
func (v value) SetIndex(i int, x interface{}) {
|
|
81
|
-
panic(
|
|
72
|
+
panic("wasm required")
|
|
82
73
|
}
|
|
83
74
|
|
|
84
75
|
func (v value) String() string {
|
|
85
|
-
panic(
|
|
76
|
+
panic("wasm required")
|
|
86
77
|
}
|
|
87
78
|
|
|
88
79
|
func (v value) Truthy() bool {
|
|
89
|
-
panic(
|
|
80
|
+
panic("wasm required")
|
|
90
81
|
}
|
|
91
82
|
|
|
92
83
|
func (v value) Type() Type {
|
|
93
|
-
panic(
|
|
84
|
+
panic("wasm required")
|
|
94
85
|
}
|
|
95
86
|
|
|
96
87
|
func null() Value {
|
|
97
|
-
panic(
|
|
88
|
+
panic("wasm required")
|
|
98
89
|
}
|
|
99
90
|
|
|
100
91
|
func undefined() Value {
|
|
101
|
-
panic(
|
|
92
|
+
panic("wasm required")
|
|
102
93
|
}
|
|
103
94
|
|
|
104
95
|
func valueOf(x interface{}) Value {
|
|
105
|
-
panic(
|
|
96
|
+
panic("wasm required")
|
|
106
97
|
}
|
|
107
98
|
|
|
108
99
|
func funcOf(fn func(this Value, args []Value) interface{}) Func {
|
|
109
|
-
panic(
|
|
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(
|
|
108
|
+
panic("wasm required")
|
|
118
109
|
}
|
|
119
110
|
|
|
120
111
|
func (w browserWindow) Size() (width, height int) {
|
|
121
|
-
panic(
|
|
112
|
+
panic("wasm required")
|
|
122
113
|
}
|
|
123
114
|
|
|
124
115
|
func (w browserWindow) CursorPosition() (x, y int) {
|
|
125
|
-
panic(
|
|
116
|
+
panic("wasm required")
|
|
126
117
|
}
|
|
127
118
|
|
|
128
119
|
func (w browserWindow) setCursorPosition(x, y int) {
|
|
129
|
-
panic(
|
|
120
|
+
panic("wasm required")
|
|
130
121
|
}
|
|
131
122
|
|
|
132
123
|
func (w *browserWindow) GetElementByID(id string) Value {
|
|
133
|
-
panic(
|
|
124
|
+
panic("wasm required")
|
|
134
125
|
}
|
|
135
126
|
|
|
136
127
|
func (w *browserWindow) ScrollToID(id string) {
|
|
137
|
-
panic(
|
|
128
|
+
panic("wasm required")
|
|
138
129
|
}
|
|
139
130
|
|
|
140
131
|
func (w *browserWindow) AddEventListener(event string, h EventHandler) func() {
|
|
141
|
-
panic(
|
|
132
|
+
panic("wasm required")
|
|
142
133
|
}
|
|
143
134
|
|
|
144
135
|
func copyBytesToGo(dst []byte, src Value) int {
|
|
145
|
-
panic(
|
|
136
|
+
panic("wasm required")
|
|
146
137
|
}
|
|
147
138
|
|
|
148
139
|
func copyBytesToJS(dst Value, src []byte) int {
|
|
149
|
-
panic(
|
|
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
|
+
}
|