lots of routign stuff
This commit is contained in:
parent
2514708522
commit
35ee5959b3
34 changed files with 1891 additions and 222 deletions
51
.air.toml
Normal file
51
.air.toml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ."
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[proxy]
|
||||
app_port = 0
|
||||
enabled = false
|
||||
proxy_port = 0
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="dataSourceStorageLocal" created-in="GO-233.13135.104">
|
||||
<component name="dataSourceStorageLocal" created-in="GO-242.21829.165">
|
||||
<data-source name="site.db" uuid="6b63d5bd-e451-4904-b659-21db5c54c16d">
|
||||
<database-info product="SQLite" version="3.40.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.40.1.0" dbms="SQLITE" exact-version="3.40.1" exact-driver-version="3.40">
|
||||
<identifier-quote-string>"</identifier-quote-string>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<dataSource name="site.db">
|
||||
<database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.51">
|
||||
<root id="1">
|
||||
<ServerVersion>3.40.1</ServerVersion>
|
||||
</root>
|
||||
<database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.53">
|
||||
<root id="1"/>
|
||||
<collation id="2" parent="1" name="RTRIM"/>
|
||||
<collation id="3" parent="1" name="NOCASE"/>
|
||||
<collation id="4" parent="1" name="BINARY"/>
|
||||
|
|
@ -1428,34 +1426,34 @@
|
|||
</table>
|
||||
<table id="505" parent="173" name="users"/>
|
||||
<column id="506" parent="504" name="type">
|
||||
<DasType>TEXT|0s</DasType>
|
||||
<Position>1</Position>
|
||||
<StoredType>TEXT|0s</StoredType>
|
||||
</column>
|
||||
<column id="507" parent="504" name="name">
|
||||
<DasType>TEXT|0s</DasType>
|
||||
<Position>2</Position>
|
||||
<StoredType>TEXT|0s</StoredType>
|
||||
</column>
|
||||
<column id="508" parent="504" name="tbl_name">
|
||||
<DasType>TEXT|0s</DasType>
|
||||
<Position>3</Position>
|
||||
<StoredType>TEXT|0s</StoredType>
|
||||
</column>
|
||||
<column id="509" parent="504" name="rootpage">
|
||||
<DasType>INT|0s</DasType>
|
||||
<Position>4</Position>
|
||||
<StoredType>INT|0s</StoredType>
|
||||
</column>
|
||||
<column id="510" parent="504" name="sql">
|
||||
<DasType>TEXT|0s</DasType>
|
||||
<Position>5</Position>
|
||||
<StoredType>TEXT|0s</StoredType>
|
||||
</column>
|
||||
<column id="511" parent="505" name="id">
|
||||
<DasType>text|0s</DasType>
|
||||
<NotNull>1</NotNull>
|
||||
<Position>1</Position>
|
||||
<StoredType>text|0s</StoredType>
|
||||
</column>
|
||||
<column id="512" parent="505" name="name">
|
||||
<DasType>text|0s</DasType>
|
||||
<NotNull>1</NotNull>
|
||||
<Position>2</Position>
|
||||
<StoredType>text|0s</StoredType>
|
||||
</column>
|
||||
<index id="513" parent="505" name="sqlite_autoindex_users_1">
|
||||
<ColNames>id</ColNames>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ func Connect() *redis.Client {
|
|||
return rdb
|
||||
}
|
||||
|
||||
func Incr(key string) int64 {
|
||||
db := Connect()
|
||||
result := db.Incr(context.Background(), key)
|
||||
return result.Val()
|
||||
}
|
||||
|
||||
func Set[T any](key string, value T) error {
|
||||
db := Connect()
|
||||
serialized, err := json.Marshal(value)
|
||||
|
|
@ -55,6 +61,45 @@ func HSet[T any](set string, key string, value T) error {
|
|||
return result.Err()
|
||||
}
|
||||
|
||||
func HIncr(set string, key string) int64 {
|
||||
db := Connect()
|
||||
result := db.HIncrBy(context.Background(), set, key, 1)
|
||||
return result.Val()
|
||||
}
|
||||
|
||||
func HGet[T any](set string, key string) *T {
|
||||
db := Connect()
|
||||
val, err := db.HGet(context.Background(), set, key).Result()
|
||||
if err != nil || val == "" {
|
||||
return nil
|
||||
}
|
||||
result := new(T)
|
||||
err = json.Unmarshal([]byte(val), result)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func GetOrSet[T any](key string, cb func() T) (*T, error) {
|
||||
db := Connect()
|
||||
val, err := db.Get(context.Background(), key).Result()
|
||||
if err == nil {
|
||||
result := new(T)
|
||||
err = json.Unmarshal([]byte(val), result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
value := cb()
|
||||
err = Set(key, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &value, nil
|
||||
}
|
||||
|
||||
func Get[T any](key string) (*T, error) {
|
||||
db := Connect()
|
||||
val, err := db.Get(context.Background(), key).Result()
|
||||
|
|
|
|||
30
go.mod
30
go.mod
|
|
@ -3,26 +3,24 @@ module mhtml
|
|||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/gofiber/fiber/v2 v2.52.4
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/redis/go-redis/v9 v9.0.5
|
||||
golang.org/x/tools v0.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gofiber/fiber/v2 v2.47.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/klauspost/compress v1.16.3 // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/redis/go-redis/v9 v9.0.5 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/samber/lo v1.38.1 // indirect
|
||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/tinylib/msgp v1.1.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.47.0 // indirect
|
||||
github.com/valyala/fasthttp v1.52.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
)
|
||||
|
|
|
|||
100
go.sum
100
go.sum
|
|
@ -1,103 +1,39 @@
|
|||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
|
||||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/gofiber/fiber/v2 v2.47.0 h1:EN5lHVCc+Pyqh5OEsk8fzRiifgwpbrP0rulQ4iNf3fs=
|
||||
github.com/gofiber/fiber/v2 v2.47.0/go.mod h1:mbFMVN1lQuzziTkkakgtKKdjfsXSw9BKR5lmcNksUoU=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
|
||||
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
|
||||
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=
|
||||
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
|
||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
|
||||
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
||||
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
|
||||
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
|
||||
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4=
|
||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
|||
71
h/app.go
Normal file
71
h/app.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package h
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
LiveReload bool
|
||||
Fiber *fiber.App
|
||||
}
|
||||
|
||||
var instance *App
|
||||
|
||||
func GetApp() *App {
|
||||
if instance == nil {
|
||||
panic("App instance not initialized")
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
func Start(app *fiber.App, opts App) {
|
||||
instance = &opts
|
||||
instance.start(app)
|
||||
}
|
||||
|
||||
func (a App) start(app *fiber.App) {
|
||||
|
||||
a.Fiber = app
|
||||
|
||||
if a.LiveReload {
|
||||
AddLiveReloadHandler("/livereload", a.Fiber)
|
||||
}
|
||||
|
||||
err := a.Fiber.Listen(":3000")
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func HtmlView(c *fiber.Ctx, page *Page) error {
|
||||
root := page.Root
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||
|
||||
if GetApp().LiveReload && root.tag == "html" {
|
||||
root.AppendChild(
|
||||
LiveReload(),
|
||||
)
|
||||
}
|
||||
|
||||
return c.SendString(
|
||||
Render(
|
||||
root,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func PartialView(c *fiber.Ctx, partial *Partial) error {
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||
if partial.Headers != nil {
|
||||
for s, a := range *partial.Headers {
|
||||
c.Set(s, a)
|
||||
}
|
||||
}
|
||||
|
||||
return c.SendString(
|
||||
Render(
|
||||
partial.Root,
|
||||
),
|
||||
)
|
||||
}
|
||||
59
h/base.go
Normal file
59
h/base.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package h
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type Headers = map[string]string
|
||||
|
||||
type Partial struct {
|
||||
Headers *Headers
|
||||
Root *Node
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Root *Node
|
||||
HttpMethod string
|
||||
}
|
||||
|
||||
func NewPage(root *Node) *Page {
|
||||
return &Page{
|
||||
HttpMethod: http.MethodGet,
|
||||
Root: root,
|
||||
}
|
||||
}
|
||||
|
||||
func NewPageWithHttpMethod(httpMethod string, root *Node) *Page {
|
||||
return &Page{
|
||||
HttpMethod: httpMethod,
|
||||
Root: root,
|
||||
}
|
||||
}
|
||||
|
||||
func NewPartialWithHeaders(headers *Headers, root *Node) *Partial {
|
||||
return &Partial{
|
||||
Headers: headers,
|
||||
Root: root,
|
||||
}
|
||||
}
|
||||
|
||||
func NewPartial(root *Node) *Partial {
|
||||
return &Partial{
|
||||
Root: root,
|
||||
}
|
||||
}
|
||||
|
||||
func GetFunctionName(i interface{}) string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
||||
}
|
||||
|
||||
func GetPartialPath(partial func(ctx *fiber.Ctx) *Partial) string {
|
||||
return GetFunctionName(partial)
|
||||
}
|
||||
|
||||
func GetPartialPathWithQs(partial func(ctx *fiber.Ctx) *Partial, qs string) string {
|
||||
return GetPartialPath(partial) + "?" + qs
|
||||
}
|
||||
33
h/livereload.go
Normal file
33
h/livereload.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package h
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Version = time.Now().Nanosecond()
|
||||
|
||||
func LiveReloadHandler(c *fiber.Ctx) error {
|
||||
v := strconv.FormatInt(int64(Version), 10)
|
||||
current := c.Cookies("version", v)
|
||||
|
||||
if current != v {
|
||||
c.Set("HX-Refresh", "true")
|
||||
}
|
||||
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "version",
|
||||
Value: v,
|
||||
})
|
||||
|
||||
return c.SendString("")
|
||||
}
|
||||
|
||||
func LiveReload() *Node {
|
||||
return Div(Get("/livereload"), Trigger("every 100ms"))
|
||||
}
|
||||
|
||||
func AddLiveReloadHandler(path string, app *fiber.App) {
|
||||
app.Get(path, LiveReloadHandler)
|
||||
}
|
||||
35
h/render.go
35
h/render.go
|
|
@ -3,9 +3,13 @@ package h
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const FlagSkip = "skip"
|
||||
const FlagText = "text"
|
||||
const FlagRaw = "raw"
|
||||
const FlagAttributeList = "attribute-list"
|
||||
|
||||
type Builder struct {
|
||||
builder *strings.Builder
|
||||
|
|
@ -35,11 +39,26 @@ func (page Builder) renderNode(node *Node) {
|
|||
}
|
||||
|
||||
for _, child := range node.children {
|
||||
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if child.tag == "class" {
|
||||
insertAttribute(node, "class", child.value)
|
||||
child.tag = FlagSkip
|
||||
}
|
||||
|
||||
if child.tag == FlagAttributeList {
|
||||
for _, gc := range child.children {
|
||||
for key, value := range gc.attributes {
|
||||
insertAttribute(node, key, value)
|
||||
}
|
||||
gc.tag = FlagSkip
|
||||
}
|
||||
child.tag = FlagSkip
|
||||
}
|
||||
|
||||
if child.tag == "attribute" {
|
||||
for key, value := range child.attributes {
|
||||
insertAttribute(node, key, value)
|
||||
|
|
@ -68,6 +87,19 @@ func (page Builder) renderNode(node *Node) {
|
|||
}
|
||||
}
|
||||
for _, child := range node.children {
|
||||
|
||||
if child == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if child.tag == FlagText {
|
||||
page.builder.WriteString(child.text)
|
||||
continue
|
||||
}
|
||||
if child.tag == FlagRaw {
|
||||
page.builder.WriteString(child.value)
|
||||
continue
|
||||
}
|
||||
if child.tag != FlagSkip {
|
||||
page.renderNode(child)
|
||||
}
|
||||
|
|
@ -78,6 +110,7 @@ func (page Builder) renderNode(node *Node) {
|
|||
}
|
||||
|
||||
func Render(node *Node) string {
|
||||
start := time.Now()
|
||||
builder := strings.Builder{}
|
||||
page := Builder{
|
||||
builder: &builder,
|
||||
|
|
@ -85,5 +118,7 @@ func Render(node *Node) string {
|
|||
}
|
||||
page.render()
|
||||
d := page.builder.String()
|
||||
duration := time.Since(start)
|
||||
fmt.Printf("render took %s\n", duration)
|
||||
return d
|
||||
}
|
||||
|
|
|
|||
35
h/state.go
Normal file
35
h/state.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package h
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"mhtml/database"
|
||||
)
|
||||
|
||||
func SessionSet(ctx *fiber.Ctx, key string, value string) error {
|
||||
sessionId := getSessionId(ctx)
|
||||
if sessionId == "" {
|
||||
return nil
|
||||
}
|
||||
return database.HSet(fmt.Sprintf("session:%s", sessionId), key, value)
|
||||
}
|
||||
|
||||
func SessionIncr(ctx *fiber.Ctx, key string) int64 {
|
||||
sessionId := getSessionId(ctx)
|
||||
if sessionId == "" {
|
||||
return 0
|
||||
}
|
||||
return database.HIncr(fmt.Sprintf("session:%s", sessionId), key)
|
||||
}
|
||||
|
||||
func SessionGet[T any](ctx *fiber.Ctx, key string) *T {
|
||||
sessionId := getSessionId(ctx)
|
||||
if sessionId == "" {
|
||||
return nil
|
||||
}
|
||||
return database.HGet[T](fmt.Sprintf("session:%s", sessionId), key)
|
||||
}
|
||||
|
||||
func getSessionId(ctx *fiber.Ctx) string {
|
||||
return ctx.Cookies("mhtml-session")
|
||||
}
|
||||
168
h/tag.go
168
h/tag.go
|
|
@ -1,20 +1,80 @@
|
|||
package h
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"html"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
id string
|
||||
tag string
|
||||
attributes map[string]string
|
||||
children []*Node
|
||||
text string
|
||||
value string
|
||||
changed bool
|
||||
}
|
||||
|
||||
func Class(value string) *Node {
|
||||
func NewNode(tag string) Node {
|
||||
return Node{
|
||||
tag: tag,
|
||||
attributes: nil,
|
||||
children: nil,
|
||||
text: "",
|
||||
value: "",
|
||||
id: "",
|
||||
}
|
||||
}
|
||||
|
||||
type Action struct {
|
||||
Type string
|
||||
Target *Node
|
||||
Value any
|
||||
}
|
||||
|
||||
func (node *Node) AppendChild(child *Node) *Node {
|
||||
node.children = append(node.children, child)
|
||||
return node
|
||||
}
|
||||
|
||||
func (node *Node) SetChanged(changed bool) *Node {
|
||||
node.changed = changed
|
||||
return node
|
||||
}
|
||||
|
||||
func Data(data map[string]any) *Node {
|
||||
serialized, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return Empty()
|
||||
}
|
||||
return Attribute("x-data", string(serialized))
|
||||
}
|
||||
|
||||
func ClassIf(condition bool, value string) *Node {
|
||||
if condition {
|
||||
return Class(value)
|
||||
}
|
||||
return Empty()
|
||||
}
|
||||
|
||||
func Class(value ...string) *Node {
|
||||
return &Node{
|
||||
tag: "class",
|
||||
value: value,
|
||||
value: MergeClasses(value...),
|
||||
}
|
||||
}
|
||||
|
||||
func MergeClasses(classes ...string) string {
|
||||
builder := ""
|
||||
for _, s := range classes {
|
||||
builder += s + " "
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
func Id(value string) *Node {
|
||||
return Attribute("id", value)
|
||||
}
|
||||
|
|
@ -28,8 +88,43 @@ func Attribute(key string, value string) *Node {
|
|||
}
|
||||
}
|
||||
|
||||
func Get(url string) *Node {
|
||||
return Attribute("hx-get", url)
|
||||
func Get(path string) *Node {
|
||||
return Attribute("hx-get", path)
|
||||
}
|
||||
|
||||
func CreateTriggers(triggers ...string) []string {
|
||||
return triggers
|
||||
}
|
||||
|
||||
type ReloadParams struct {
|
||||
Triggers []string
|
||||
}
|
||||
|
||||
func View(partial func(ctx *fiber.Ctx) *Partial, params ReloadParams) *Node {
|
||||
return &Node{
|
||||
tag: "attribute",
|
||||
attributes: map[string]string{
|
||||
"hx-get": GetPartialPath(partial),
|
||||
"hx-trigger": strings.Join(params.Triggers, ", "),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func GetWithQs(path string, qs map[string]string) *Node {
|
||||
u, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return Empty()
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
|
||||
for s := range qs {
|
||||
q.Add(s, qs[s])
|
||||
}
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return Get(u.String())
|
||||
}
|
||||
|
||||
func Post(url string) *Node {
|
||||
|
|
@ -40,6 +135,13 @@ func Trigger(trigger string) *Node {
|
|||
return Attribute("hx-trigger", trigger)
|
||||
}
|
||||
|
||||
func Text(text string) *Node {
|
||||
return &Node{
|
||||
tag: "text",
|
||||
text: text,
|
||||
}
|
||||
}
|
||||
|
||||
func Target(target string) *Node {
|
||||
return Attribute("hx-target", target)
|
||||
}
|
||||
|
|
@ -72,7 +174,11 @@ func Click(value string) *Node {
|
|||
return Attribute("onclick", value)
|
||||
}
|
||||
|
||||
func Page(children ...*Node) *Node {
|
||||
func OnClickWs(handler string) *Node {
|
||||
return Attribute("data-ws-click", handler)
|
||||
}
|
||||
|
||||
func Html(children ...*Node) *Node {
|
||||
return &Node{
|
||||
tag: "html",
|
||||
children: children,
|
||||
|
|
@ -103,6 +209,18 @@ func Script(url string) *Node {
|
|||
}
|
||||
}
|
||||
|
||||
func Raw(text string) *Node {
|
||||
return &Node{
|
||||
tag: "raw",
|
||||
children: make([]*Node, 0),
|
||||
value: text,
|
||||
}
|
||||
}
|
||||
|
||||
func RawScript(text string) *Node {
|
||||
return Raw("<script>" + text + "</script>")
|
||||
}
|
||||
|
||||
func Div(children ...*Node) *Node {
|
||||
return &Node{
|
||||
tag: "div",
|
||||
|
|
@ -158,6 +276,19 @@ func Fragment(children ...*Node) *Node {
|
|||
}
|
||||
}
|
||||
|
||||
func AttributeList(children ...*Node) *Node {
|
||||
return &Node{
|
||||
tag: FlagAttributeList,
|
||||
children: children,
|
||||
}
|
||||
}
|
||||
|
||||
func AppendChildren(node *Node, children ...*Node) *Node {
|
||||
node.children = append(node.children, children...)
|
||||
return node
|
||||
|
||||
}
|
||||
|
||||
func Button(children ...*Node) *Node {
|
||||
return &Node{
|
||||
tag: "button",
|
||||
|
|
@ -165,6 +296,10 @@ func Button(children ...*Node) *Node {
|
|||
}
|
||||
}
|
||||
|
||||
func Indicator(tag string) *Node {
|
||||
return Attribute("hx-indicator", tag)
|
||||
}
|
||||
|
||||
func P(text string, children ...*Node) *Node {
|
||||
return &Node{
|
||||
tag: "p",
|
||||
|
|
@ -187,6 +322,16 @@ func Empty() *Node {
|
|||
}
|
||||
}
|
||||
|
||||
func BeforeRequestSetHtml(children ...*Node) *Node {
|
||||
serialized := Render(Fragment(children...))
|
||||
return Attribute("hx-on::before-request", `this.innerHTML = '`+html.EscapeString(serialized)+`'`)
|
||||
}
|
||||
|
||||
func AfterRequestSetHtml(children ...*Node) *Node {
|
||||
serialized := Render(Fragment(children...))
|
||||
return Attribute("hx-on::after-request", `this.innerHTML = '`+html.EscapeString(serialized)+`'`)
|
||||
}
|
||||
|
||||
func If(condition bool, node *Node) *Node {
|
||||
if condition {
|
||||
return node
|
||||
|
|
@ -195,6 +340,19 @@ func If(condition bool, node *Node) *Node {
|
|||
}
|
||||
}
|
||||
|
||||
func JsIf(condition string, node *Node) *Node {
|
||||
node1 := &Node{tag: "template"}
|
||||
node1.AppendChild(Attribute("x-if", condition))
|
||||
node1.AppendChild(node)
|
||||
return node
|
||||
}
|
||||
|
||||
func JsIfElse(condition string, node *Node, node2 *Node) *Node {
|
||||
node1Template := Div(Attribute("x-show", condition), node)
|
||||
node2Template := Div(Attribute("x-show", "!("+condition+")"), node2)
|
||||
return Fragment(node1Template, node2Template)
|
||||
}
|
||||
|
||||
func IfElse(condition bool, node *Node, node2 *Node) *Node {
|
||||
if condition {
|
||||
return node
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package httpjson
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
|
@ -21,10 +23,6 @@ func getClient() *http.Client {
|
|||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: tr,
|
||||
// do not follow redirects
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
client = httpClient
|
||||
})
|
||||
|
|
@ -43,6 +41,10 @@ func Get[T any](url string) (T, error) {
|
|||
resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
return *new(T), errors.New(fmt.Sprintf("get to %s failed with %d code", url, resp.StatusCode))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return *new(T), err
|
||||
|
|
@ -52,5 +54,10 @@ func Get[T any](url string) (T, error) {
|
|||
if err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
|
||||
if d == nil {
|
||||
return *new(T), errors.New("failed to create T")
|
||||
}
|
||||
|
||||
return *d, nil
|
||||
}
|
||||
|
|
|
|||
24
index.html
24
index.html
|
|
@ -1,24 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex flex-col gap-2 flex flex-col gap-2 bg-gray-100 h-full w-full items-center p-12">
|
||||
<div>
|
||||
<p>Log in
|
||||
<button></button>
|
||||
</p>
|
||||
<div class="flex flex-col gap-2 items-center"><p>Sydne Anschutz</p>
|
||||
<p>sanschutz0808@gmail.com</p>
|
||||
<button class="bg-red-200 rounded p-2" onclick="alert(1)"><p>Delete</p></button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 items-center"><p>Maddy</p>
|
||||
<p>jm@madev.me</p>
|
||||
<button class="bg-red-200 rounded p-2" onclick="alert(1)"><p>Delete</p></button>
|
||||
</div>
|
||||
</div>
|
||||
<div><p>Enter name</p><input type="text" class="rounded p-4 border-gray-100"></input></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
10
js/mhtml.js
Normal file
10
js/mhtml.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Replace 'ws://example.com/socket' with the URL of your WebSocket server
|
||||
|
||||
window.onload = function () {
|
||||
document.querySelectorAll('[m\\:onclick]').forEach(element => {
|
||||
const value = element.getAttribute('m:onclick')
|
||||
element.addEventListener('click', () => {
|
||||
fetch('/click/' + value).catch()
|
||||
})
|
||||
});
|
||||
}
|
||||
9
justfile
Normal file
9
justfile
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Command to run and watch the Go application using Air
|
||||
run-app:
|
||||
air
|
||||
|
||||
run-gen:
|
||||
go run ./tooling/astgen
|
||||
|
||||
watch-gen:
|
||||
go run ./tooling/watch.go --command 'go run ./tooling/astgen'
|
||||
95
main.go
95
main.go
|
|
@ -2,85 +2,36 @@ package main
|
|||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"mhtml/database"
|
||||
"github.com/google/uuid"
|
||||
"mhtml/h"
|
||||
"mhtml/news"
|
||||
"strconv"
|
||||
"time"
|
||||
"mhtml/pages"
|
||||
"mhtml/partials"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
|
||||
var Version = time.Now().Nanosecond()
|
||||
|
||||
func LiveReloadHandler(c *fiber.Ctx) error {
|
||||
v := strconv.FormatInt(int64(Version), 10)
|
||||
current := c.Cookies("version", v)
|
||||
|
||||
if current != v {
|
||||
c.Set("HX-Refresh", "true")
|
||||
}
|
||||
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "version",
|
||||
Value: v,
|
||||
})
|
||||
return c.SendString("")
|
||||
}
|
||||
|
||||
func Page(children ...*h.Node) *h.Node {
|
||||
return h.Page(
|
||||
h.Head(
|
||||
h.Script("https://cdn.tailwindcss.com"),
|
||||
h.Script("https://unpkg.com/htmx.org@1.9.2"),
|
||||
),
|
||||
h.Body(
|
||||
h.VStack(
|
||||
h.Class("flex flex-col gap-2 bg-gray-100 h-full w-full items-center p-12"),
|
||||
h.Fragment(children...),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func IndexPage(c *fiber.Ctx) error {
|
||||
page := Page(
|
||||
h.Div(
|
||||
h.P("Hacker News - Top Stories"),
|
||||
),
|
||||
news.StoryList(),
|
||||
)
|
||||
return HtmlView(c, page)
|
||||
}
|
||||
|
||||
func HtmlView(c *fiber.Ctx, child *h.Node) error {
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||
return c.SendString(
|
||||
h.Render(
|
||||
child,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func main() {
|
||||
f := fiber.New()
|
||||
f.Static("/js", "./js")
|
||||
|
||||
database.HSet("users", "sydne", User{
|
||||
Name: "Sydne Anschutz",
|
||||
Email: "sanschutz0808@gmail.com",
|
||||
f.Use(func(ctx *fiber.Ctx) error {
|
||||
if ctx.Cookies("mhtml-session") != "" {
|
||||
return ctx.Next()
|
||||
}
|
||||
id := ctx.IP() + uuid.NewString()
|
||||
ctx.Cookie(&fiber.Cookie{
|
||||
Name: "mhtml-session",
|
||||
Value: id,
|
||||
SessionOnly: true,
|
||||
})
|
||||
return ctx.Next()
|
||||
})
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
app.Get("/", IndexPage)
|
||||
app.Get("/news/:id", func(c *fiber.Ctx) error {
|
||||
return HtmlView(c, Page(
|
||||
news.StoryFull(c.Params("id")),
|
||||
))
|
||||
f.Get("/mhtml/partials.*", func(ctx *fiber.Ctx) error {
|
||||
return h.PartialView(ctx, partials.GetPartialFromContext(ctx))
|
||||
})
|
||||
app.Get("/livereload", LiveReloadHandler)
|
||||
|
||||
app.Listen(":3000")
|
||||
pages.RegisterPages(f)
|
||||
|
||||
h.Start(f, h.App{
|
||||
LiveReload: true,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,22 +2,23 @@ package news
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"mhtml/database"
|
||||
"mhtml/h"
|
||||
)
|
||||
|
||||
func StoryList() *h.Node {
|
||||
posts, err := List()
|
||||
|
||||
if err != nil {
|
||||
return h.P(err.Error())
|
||||
}
|
||||
posts, _ := database.GetOrSet[[]Post]("posts", func() []Post {
|
||||
p, _ := List()
|
||||
return p
|
||||
})
|
||||
|
||||
if len(posts) == 0 {
|
||||
if len(*posts) == 0 {
|
||||
return h.P("No results found")
|
||||
}
|
||||
|
||||
return h.Fragment(
|
||||
h.VStack(h.List(posts, func(item Post) *h.Node {
|
||||
h.VStack(h.List(*posts, func(item Post) *h.Node {
|
||||
return StoryCard(item)
|
||||
})),
|
||||
)
|
||||
|
|
@ -26,7 +27,7 @@ func StoryList() *h.Node {
|
|||
func StoryCard(post Post) *h.Node {
|
||||
url := fmt.Sprintf("/news/%d", post.Id)
|
||||
return h.VStack(
|
||||
h.Class("items-center bg-red-200 p-4 rounded"),
|
||||
h.Class("items-center bg-indigo-200 p-4 rounded"),
|
||||
h.A(post.Title, h.Href(url)),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
22
pages/base/root.go
Normal file
22
pages/base/root.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"mhtml/h"
|
||||
)
|
||||
|
||||
func RootPage(children ...*h.Node) *h.Node {
|
||||
return h.Html(
|
||||
h.Head(
|
||||
h.Script("https://cdn.tailwindcss.com"),
|
||||
h.Script("https://unpkg.com/htmx.org@1.9.12"),
|
||||
h.Script("https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"),
|
||||
h.Script("/js/mhtml.js"),
|
||||
),
|
||||
h.Body(
|
||||
h.VStack(
|
||||
h.Class("flex flex-col gap-2 bg-gray-100 h-full"),
|
||||
h.Fragment(children...),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
17
pages/generated.go
Normal file
17
pages/generated.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Package pages THIS FILE IS GENERATED. DO NOT EDIT.
|
||||
package pages
|
||||
|
||||
import "github.com/gofiber/fiber/v2"
|
||||
import "mhtml/h"
|
||||
|
||||
func RegisterPages(f *fiber.App) {
|
||||
f.Get("/", func(ctx *fiber.Ctx) error {
|
||||
return h.HtmlView(ctx, IndexPage(ctx))
|
||||
})
|
||||
f.Get("/news/:id", func(ctx *fiber.Ctx) error {
|
||||
return h.HtmlView(ctx, Test(ctx))
|
||||
})
|
||||
f.Get("/news", func(ctx *fiber.Ctx) error {
|
||||
return h.HtmlView(ctx, ListPage(ctx))
|
||||
})
|
||||
}
|
||||
20
pages/index.go
Normal file
20
pages/index.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"mhtml/h"
|
||||
"mhtml/pages/base"
|
||||
)
|
||||
|
||||
func IndexPage(c *fiber.Ctx) *h.Page {
|
||||
return h.NewPage(base.RootPage(
|
||||
h.Fragment(
|
||||
h.Div(
|
||||
h.Class("inline-flex flex-col gap-4 p-4"),
|
||||
h.Div(
|
||||
h.Class("max-w-md flex flex-col gap-4"),
|
||||
h.P("Routes"),
|
||||
),
|
||||
)),
|
||||
))
|
||||
}
|
||||
14
pages/news.$id.go
Normal file
14
pages/news.$id.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"mhtml/h"
|
||||
)
|
||||
|
||||
func Test(ctx *fiber.Ctx) *h.Page {
|
||||
text := fmt.Sprintf("News ID: %s", ctx.Params("id"))
|
||||
return h.NewPage(
|
||||
h.Div(h.Text(text)),
|
||||
)
|
||||
}
|
||||
42
pages/news.index.go
Normal file
42
pages/news.index.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"mhtml/h"
|
||||
"mhtml/pages/base"
|
||||
"mhtml/partials"
|
||||
"mhtml/ui"
|
||||
)
|
||||
|
||||
func ListPage(ctx *fiber.Ctx) *h.Page {
|
||||
return h.NewPage(base.RootPage(
|
||||
list(),
|
||||
))
|
||||
}
|
||||
|
||||
func list() *h.Node {
|
||||
return h.Fragment(
|
||||
partials.SheetClosed(),
|
||||
h.Div(
|
||||
h.Class("inline-flex flex-col gap-4 p-4"),
|
||||
h.Div(
|
||||
h.Class("max-w-md flex flex-col gap-4 "),
|
||||
openButton(),
|
||||
),
|
||||
h.Div(
|
||||
h.View(partials.SheetOpenCount, h.ReloadParams{
|
||||
Triggers: h.CreateTriggers("load", "sheetOpened from:body"),
|
||||
}),
|
||||
h.Text("you opened sheet 0 times")),
|
||||
))
|
||||
}
|
||||
|
||||
func openButton() *h.Node {
|
||||
return h.VStack(
|
||||
ui.PrimaryButton(ui.ButtonProps{
|
||||
Text: "Open Sheet",
|
||||
Target: "#sheet-partial",
|
||||
Get: h.GetPartialPathWithQs(partials.Sheet, "open=true"),
|
||||
}),
|
||||
)
|
||||
}
|
||||
1
partials/base.go
Normal file
1
partials/base.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package partials
|
||||
16
partials/generated.go
Normal file
16
partials/generated.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Package partials THIS FILE IS GENERATED. DO NOT EDIT.
|
||||
package partials
|
||||
|
||||
import "mhtml/h"
|
||||
import "github.com/gofiber/fiber/v2"
|
||||
|
||||
func GetPartialFromContext(ctx *fiber.Ctx) *h.Partial {
|
||||
path := ctx.Path()
|
||||
if path == "SheetOpenCount" || path == "/mhtml/partials.SheetOpenCount" {
|
||||
return SheetOpenCount(ctx)
|
||||
}
|
||||
if path == "Sheet" || path == "/mhtml/partials.Sheet" {
|
||||
return Sheet(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
56
partials/sheet.go
Normal file
56
partials/sheet.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package partials
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"mhtml/h"
|
||||
"mhtml/news"
|
||||
"mhtml/ui"
|
||||
)
|
||||
|
||||
func SheetOpenCount(ctx *fiber.Ctx) *h.Partial {
|
||||
rnd := h.SessionGet[int64](ctx, "sheet-open-count")
|
||||
if rnd == nil {
|
||||
rnd = new(int64)
|
||||
}
|
||||
return h.NewPartial(h.Div(
|
||||
h.Text(fmt.Sprintf("you opened sheet %d times", *rnd)),
|
||||
))
|
||||
}
|
||||
|
||||
func SheetClosed() *h.Node {
|
||||
return h.Div(h.Id("sheet-partial"))
|
||||
}
|
||||
|
||||
func Sheet(ctx *fiber.Ctx) *h.Partial {
|
||||
open := ctx.Query("open")
|
||||
if open == "true" {
|
||||
h.SessionIncr(ctx, "sheet-open-count")
|
||||
}
|
||||
return h.NewPartialWithHeaders(
|
||||
&map[string]string{
|
||||
"hx-trigger": "sheetOpened",
|
||||
},
|
||||
h.IfElse(open == "true", SheetOpen(), SheetClosed()),
|
||||
)
|
||||
}
|
||||
|
||||
func SheetOpen() *h.Node {
|
||||
return h.Div(
|
||||
h.Class(`fixed top-0 right-0 h-full w-96 bg-gray-100 shadow-lg z-50`),
|
||||
h.Div(
|
||||
h.Class("p-4 overflow-y-auto h-full w-full flex flex-col gap-4"),
|
||||
h.P("My Sheet",
|
||||
h.Class("text-lg font-bold"),
|
||||
),
|
||||
h.P("This is a sheet",
|
||||
h.Class("text-sm mt-2"),
|
||||
),
|
||||
ui.Button(ui.ButtonProps{
|
||||
Text: "Close Sheet",
|
||||
Target: "#sheet-partial",
|
||||
Get: h.GetPartialPathWithQs(Sheet, "open=false"),
|
||||
}),
|
||||
news.StoryList(),
|
||||
))
|
||||
}
|
||||
135
tooling/astgen/ast.go
Normal file
135
tooling/astgen/ast.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"golang.org/x/tools/go/ast/astutil"
|
||||
)
|
||||
|
||||
type AstUtil struct {
|
||||
file *ast.File
|
||||
}
|
||||
|
||||
func NewAstUtil(file *ast.File) *AstUtil {
|
||||
return &AstUtil{file: file}
|
||||
}
|
||||
|
||||
func (a *AstUtil) Sync(builder *CodeBuilder) {
|
||||
fset := token.NewFileSet()
|
||||
// Parse the code string into an AST
|
||||
f, err := parser.ParseFile(fset, "", builder.String(), 0)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
a.file = f
|
||||
}
|
||||
|
||||
func (a *AstUtil) HasMethod(methodName string) bool {
|
||||
return a.GetMethodByName(methodName) != nil
|
||||
}
|
||||
|
||||
func (a *AstUtil) HasStruct(name string) bool {
|
||||
for _, decl := range a.file.Decls {
|
||||
_, ok := decl.(*ast.GenDecl)
|
||||
if ok {
|
||||
spec, ok := decl.(*ast.GenDecl).Specs[0].(*ast.TypeSpec)
|
||||
if ok {
|
||||
if spec.Name.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *AstUtil) GetMethodByName(name string) *ast.FuncDecl {
|
||||
for _, decl := range a.file.Decls {
|
||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||
if ok && funcDecl.Name.Name == name {
|
||||
return funcDecl
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AstUtil) GetMethodAsString(methodName string) string {
|
||||
fdecl := a.GetMethodByName(methodName)
|
||||
|
||||
if fdecl == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
fset := token.NewFileSet() // Create a FileSet to manage source file positions
|
||||
|
||||
// Use printer.Fprint to format the AST node into the buffer
|
||||
err := printer.Fprint(&buf, fset, fdecl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type UpdateSignature struct {
|
||||
MethodName string
|
||||
NewName string
|
||||
NewParams []NameType
|
||||
NewResults []ReturnType
|
||||
}
|
||||
|
||||
func (a *AstUtil) UpdateMethodSignature(req UpdateSignature) {
|
||||
|
||||
newParams := Map(req.NewParams, func(nt NameType) *ast.Field {
|
||||
return nt.ToAst()
|
||||
})
|
||||
|
||||
newResults := Map(req.NewResults, func(rt ReturnType) *ast.Field {
|
||||
return rt.ToAst()
|
||||
})
|
||||
|
||||
for _, decl := range a.file.Decls {
|
||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||
if ok && funcDecl.Name.Name == req.MethodName {
|
||||
// Method found, update its signature
|
||||
funcDecl.Name = ast.NewIdent(req.NewName)
|
||||
funcDecl.Type.Params = &ast.FieldList{List: newParams}
|
||||
funcDecl.Type.Results = &ast.FieldList{List: newResults}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AstUtil) SetPackageName(name string) {
|
||||
a.file.Name = ast.NewIdent(name)
|
||||
}
|
||||
|
||||
func (a *AstUtil) AddImport(path string) {
|
||||
if !astutil.UsesImport(a.file, path) {
|
||||
astutil.AddImport(token.NewFileSet(), a.file, path)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AstUtil) DeleteMethod(methodName string) {
|
||||
for i, decl := range a.file.Decls {
|
||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||
if ok && funcDecl.Name.Name == methodName {
|
||||
// Method found, remove it from the slice
|
||||
a.file.Decls = append(a.file.Decls[:i], a.file.Decls[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AstUtil) String() string {
|
||||
var buf bytes.Buffer
|
||||
fset := token.NewFileSet()
|
||||
err := printer.Fprint(&buf, fset, a.file)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
365
tooling/astgen/codebuilder.go
Normal file
365
tooling/astgen/codebuilder.go
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// ... (Field and Method structs remain the same)
|
||||
|
||||
type CodeBuilder struct {
|
||||
structTemplate *template.Template
|
||||
methodTemplate *template.Template
|
||||
toEventTemplate *template.Template
|
||||
commandSwitchTemplate *template.Template
|
||||
validateTemplate *template.Template
|
||||
funcTemplate *template.Template
|
||||
interfaceTemplate *template.Template
|
||||
result string
|
||||
astu *AstUtil
|
||||
}
|
||||
|
||||
type Assignment struct {
|
||||
Field string
|
||||
Value string
|
||||
Operator string
|
||||
}
|
||||
|
||||
type Function struct {
|
||||
Name string
|
||||
Parameters []NameType
|
||||
Return []ReturnType
|
||||
Body string
|
||||
}
|
||||
|
||||
type Struct struct {
|
||||
Name string
|
||||
Fields []NameType
|
||||
}
|
||||
|
||||
type Validation struct {
|
||||
MethodCall string
|
||||
}
|
||||
|
||||
type Method struct {
|
||||
StructReceiver string
|
||||
StructName string
|
||||
MethodName string
|
||||
Body string
|
||||
Parameters []NameType
|
||||
Return []ReturnType
|
||||
}
|
||||
|
||||
type NameType struct {
|
||||
Name string
|
||||
Type string
|
||||
}
|
||||
|
||||
func (nt *NameType) ToAst() *ast.Field {
|
||||
return &ast.Field{
|
||||
Names: []*ast.Ident{ast.NewIdent(nt.Name)}, // Create an identifier for the field name
|
||||
Type: ast.NewIdent(nt.Type), // Create an identifier for the field type
|
||||
}
|
||||
}
|
||||
|
||||
type ReturnType struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
func (rt *ReturnType) ToAst() *ast.Field {
|
||||
return &ast.Field{
|
||||
Type: ast.NewIdent(rt.Type),
|
||||
}
|
||||
}
|
||||
|
||||
type Case struct {
|
||||
CommandType string
|
||||
CaseBody string
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) Append(result string) *CodeBuilder {
|
||||
cb.result += "\n"
|
||||
cb.result += result
|
||||
cb.result += "\n"
|
||||
return cb
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) String() string {
|
||||
return cb.result
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) SyncFromAst() *CodeBuilder {
|
||||
cb.SetResult(cb.astu.String())
|
||||
return cb
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) SyncToAst() *CodeBuilder {
|
||||
cb.astu.Sync(cb)
|
||||
return cb
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) SetResult(result string) *CodeBuilder {
|
||||
cb.result = result
|
||||
return cb
|
||||
}
|
||||
|
||||
func NewCodeBuilder(astu *AstUtil) *CodeBuilder {
|
||||
toEventTemplate, _ := template.New("toEvent").Parse(`
|
||||
func (c *{{.StructName}}) {{.MethodCall}}() *{{.EventType}} {
|
||||
var event *{{.EventType}} = &{{.EventType}}{}
|
||||
{{range .Fields}}
|
||||
event.{{.Name}} = c.{{.Name}}
|
||||
{{end}}
|
||||
return event
|
||||
}
|
||||
`)
|
||||
|
||||
validationTemplate := template.Must(template.New("validation").Parse(`
|
||||
func (c *{{.StructName}}) {{.MethodCall}}() error {
|
||||
{{range .Validations}}if err := {{.MethodCall}}; err != nil {
|
||||
return err
|
||||
}
|
||||
{{end}}
|
||||
return nil
|
||||
}
|
||||
`))
|
||||
|
||||
methodTemplate := template.Must(template.New("method").Parse(`
|
||||
func ({{.StructReceiver}} *{{.StructName}}) {{.MethodCall}}({{range $index, $param := .Parameters}}{{if $index}}, {{end}}{{$param.Name}} {{$param.Type}}{{end}}) ({{range $index, $ret := .Return}}{{if $index}}, {{end}}{{$ret.Type}}{{end}}) {
|
||||
{{if .Body}}
|
||||
{{.Body}}
|
||||
{{else}}
|
||||
return {{range $index, $ret := .Return}}{{if $index}}, {{end}}nil{{end}}
|
||||
{{end}}
|
||||
}
|
||||
`))
|
||||
|
||||
commandSwitchTemplate, _ := template.New("commandSwitch").Parse(`
|
||||
switch c := command.(type) {
|
||||
{{range .Cases}}case *{{.CommandType}}:
|
||||
{{.CaseBody}}
|
||||
{{end}}
|
||||
}
|
||||
`)
|
||||
|
||||
structTemplate, _ := template.New("struct").Parse(`
|
||||
type {{.StructName}} struct {
|
||||
{{range .Fields}}
|
||||
{{.Name}} {{.Type}}
|
||||
{{end}}
|
||||
}
|
||||
`)
|
||||
|
||||
interfaceTemplate := template.Must(template.New("interface").Parse(`
|
||||
type {{.Name}} interface {
|
||||
{{range .Methods}}
|
||||
{{.Name}}({{range $index, $param := .Parameters}}{{if $index}}, {{end}}{{$param.Name}} {{$param.Type}}{{end}}) {{range $index, $ret := .Return}}{{if $index}}, {{end}}{{$ret.Type}}{{end}}
|
||||
{{end}}
|
||||
}
|
||||
`))
|
||||
|
||||
funcTemplateStr := `
|
||||
func {{ .Name }}({{ range $index, $param := .Parameters }}{{if $index}}, {{end}}{{$param.Name}} {{$param.Type}}{{end}}) ({{range $index, $ret := .Return }}{{if $index}}, {{end}}{{$ret.Type}}{{end}}) {
|
||||
{{.Body}}
|
||||
}
|
||||
`
|
||||
|
||||
funcTemplate := template.Must(template.New("function").Funcs(template.FuncMap{
|
||||
"trimPrefix": strings.TrimPrefix,
|
||||
}).Parse(funcTemplateStr))
|
||||
|
||||
builder := &CodeBuilder{
|
||||
funcTemplate: funcTemplate,
|
||||
structTemplate: structTemplate,
|
||||
interfaceTemplate: interfaceTemplate,
|
||||
toEventTemplate: toEventTemplate,
|
||||
methodTemplate: methodTemplate,
|
||||
validateTemplate: validationTemplate,
|
||||
commandSwitchTemplate: commandSwitchTemplate,
|
||||
astu: astu,
|
||||
}
|
||||
|
||||
if astu != nil {
|
||||
builder.SyncFromAst()
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) PrependLine(line string) {
|
||||
cb.result = line + "\n" + cb.result
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) PrependLineF(format string, args ...interface{}) {
|
||||
cb.PrependLine(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) AppendLine(line string) {
|
||||
cb.result += line + "\n"
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) PrependLineIfNotExist(format string) {
|
||||
if !strings.Contains(cb.result, format) {
|
||||
cb.PrependLine(format)
|
||||
}
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) AppendLineIfNotExist(line string) {
|
||||
if !strings.Contains(cb.result, line) {
|
||||
cb.result += line + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) HasString(str string) bool {
|
||||
return strings.Contains(cb.result, str)
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) HasStringF(str string, args ...interface{}) bool {
|
||||
return strings.Contains(cb.result, fmt.Sprintf(str, args...))
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) AppendLineF(format string, args ...interface{}) {
|
||||
cb.AppendLine(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) ExecuteTemplate(template *template.Template, data map[string]any) string {
|
||||
var buf bytes.Buffer
|
||||
err := template.Execute(&buf, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return removeAllEmptyLines(buf.String())
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) AddImport(imp string) *CodeBuilder {
|
||||
line := fmt.Sprintf(`import "%s"`, imp)
|
||||
if !strings.Contains(cb.result, line) {
|
||||
cb.AppendLine(line)
|
||||
}
|
||||
return cb
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) BuildAssignment(a Assignment) string {
|
||||
return a.Field + " " + a.Operator + " " + a.Value
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) BuildInterface(name string, methods []Function) string {
|
||||
return cb.ExecuteTemplate(cb.interfaceTemplate, map[string]any{
|
||||
"Name": name,
|
||||
"Methods": methods,
|
||||
})
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) BuildNewCommandMethod(_struct Struct) string {
|
||||
return cb.BuildFunction(Function{
|
||||
Name: fmt.Sprintf("New%s", _struct.Name),
|
||||
Parameters: _struct.Fields,
|
||||
Return: []ReturnType{
|
||||
{Type: fmt.Sprintf("*%s", _struct.Name)},
|
||||
},
|
||||
Body: cb.Template(`
|
||||
result := {{.ReturnType}}{}
|
||||
{{range .Fields}}result.{{.Name}} = {{.Name}}
|
||||
{{end}}{{if .Body}}
|
||||
{{.Body}}{{end}}
|
||||
return result
|
||||
`, map[string]any{
|
||||
"Fields": _struct.Fields,
|
||||
"ReturnType": "&" + _struct.Name,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) BuildValidation(structName string, methodName string, validations []Validation) string {
|
||||
return cb.ExecuteTemplate(cb.validateTemplate, map[string]any{
|
||||
"StructName": structName,
|
||||
"MethodCall": methodName,
|
||||
"Validations": validations,
|
||||
})
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) BuildMethod(method Method) string {
|
||||
data := map[string]any{
|
||||
"StructReceiver": method.StructReceiver,
|
||||
"StructName": method.StructName,
|
||||
"MethodCall": method.MethodName,
|
||||
"Parameters": method.Parameters,
|
||||
"Return": method.Return,
|
||||
}
|
||||
if method.Body != "" {
|
||||
data["Body"] = method.Body
|
||||
}
|
||||
return cb.ExecuteTemplate(cb.methodTemplate, data)
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) Template(templateStr string, data map[string]any) string {
|
||||
t, err := template.New("temp").Parse(templateStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cb.ExecuteTemplate(t, data)
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) BuildStruct(s Struct) string {
|
||||
return cb.ExecuteTemplate(cb.structTemplate, map[string]any{
|
||||
"StructName": s.Name,
|
||||
"Fields": s.Fields,
|
||||
})
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) BuildFunction(f Function) string {
|
||||
return cb.ExecuteTemplate(cb.funcTemplate, map[string]any{
|
||||
"Name": f.Name,
|
||||
"Parameters": f.Parameters,
|
||||
"Return": f.Return,
|
||||
"Body": f.Body,
|
||||
})
|
||||
}
|
||||
|
||||
func (cb *CodeBuilder) BuildCommandSwitch(cases []Case) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
err := cb.commandSwitchTemplate.Execute(&buf, map[string]interface{}{
|
||||
"Cases": cases,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func removeAllEmptyLines(input string) string {
|
||||
// Split the input into lines
|
||||
lines := strings.Split(input, "\n")
|
||||
|
||||
// Filter out empty lines
|
||||
var nonEmptyLines []string
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) != "" {
|
||||
nonEmptyLines = append(nonEmptyLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
// Join the non-empty lines back into a string
|
||||
output := strings.Join(nonEmptyLines, "\n")
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func Map[T any, U any](slice []T, transform func(T) U) []U {
|
||||
result := make([]U, len(slice))
|
||||
for i, v := range slice {
|
||||
result[i] = transform(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func FlatMap[T any, U any](slice []T, transform func(T) []U) []U {
|
||||
var result []U
|
||||
for _, v := range slice {
|
||||
result = append(result, transform(v)...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
268
tooling/astgen/entry.go
Normal file
268
tooling/astgen/entry.go
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Path string
|
||||
FuncName string
|
||||
Package string
|
||||
Import string
|
||||
}
|
||||
|
||||
func findPublicFuncsReturningHPartial(dir string) ([]string, error) {
|
||||
var functions []string
|
||||
|
||||
// Walk through the directory to find all Go files.
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only process Go files.
|
||||
if !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the Go file.
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Inspect the AST for function declarations.
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
// Check if the node is a function declaration.
|
||||
if funcDecl, ok := n.(*ast.FuncDecl); ok {
|
||||
// Only consider exported (public) functions.
|
||||
if funcDecl.Name.IsExported() {
|
||||
// Check the return type.
|
||||
if funcDecl.Type.Results != nil {
|
||||
for _, result := range funcDecl.Type.Results.List {
|
||||
// Check if the return type is *h.Partial.
|
||||
if starExpr, ok := result.Type.(*ast.StarExpr); ok {
|
||||
if selectorExpr, ok := starExpr.X.(*ast.SelectorExpr); ok {
|
||||
// Check if the package name is 'h' and type is 'Partial'.
|
||||
if ident, ok := selectorExpr.X.(*ast.Ident); ok && ident.Name == "h" {
|
||||
if selectorExpr.Sel.Name == "Partial" {
|
||||
functions = append(functions, funcDecl.Name.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return functions, nil
|
||||
}
|
||||
|
||||
func findPublicFuncsReturningHPage(dir string) ([]Page, error) {
|
||||
var pages = make([]Page, 0)
|
||||
|
||||
// Walk through the directory to find all Go files.
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only process Go files.
|
||||
if !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the Go file.
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Inspect the AST for function declarations.
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
// Check if the node is a function declaration.
|
||||
if funcDecl, ok := n.(*ast.FuncDecl); ok {
|
||||
// Only consider exported (public) functions.
|
||||
if funcDecl.Name.IsExported() {
|
||||
// Check the return type.
|
||||
if funcDecl.Type.Results != nil {
|
||||
for _, result := range funcDecl.Type.Results.List {
|
||||
// Check if the return type is *h.Partial.
|
||||
if starExpr, ok := result.Type.(*ast.StarExpr); ok {
|
||||
if selectorExpr, ok := starExpr.X.(*ast.SelectorExpr); ok {
|
||||
// Check if the package name is 'h' and type is 'Partial'.
|
||||
if ident, ok := selectorExpr.X.(*ast.Ident); ok && ident.Name == "h" {
|
||||
if selectorExpr.Sel.Name == "Page" {
|
||||
pages = append(pages, Page{
|
||||
Package: node.Name.Name,
|
||||
Import: fmt.Sprintf("mhtml/%s", filepath.Dir(path)),
|
||||
Path: path,
|
||||
FuncName: funcDecl.Name.Name,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pages, nil
|
||||
}
|
||||
|
||||
func buildGetPartialFromContext(builder *CodeBuilder, funcs []string) {
|
||||
fName := "GetPartialFromContext"
|
||||
|
||||
body := `
|
||||
path := ctx.Path()
|
||||
`
|
||||
|
||||
for _, f := range funcs {
|
||||
if f == fName {
|
||||
continue
|
||||
}
|
||||
body += fmt.Sprintf(`
|
||||
if path == "%s" || path == "/mhtml/partials.%s" {
|
||||
return %s(ctx)
|
||||
}
|
||||
`, f, f, f)
|
||||
}
|
||||
|
||||
body += "return nil"
|
||||
|
||||
f := Function{
|
||||
Name: fName,
|
||||
Parameters: []NameType{
|
||||
{Name: "ctx", Type: "*fiber.Ctx"},
|
||||
},
|
||||
Return: []ReturnType{
|
||||
{Type: "*h.Partial"},
|
||||
},
|
||||
Body: body,
|
||||
}
|
||||
|
||||
builder.Append(builder.BuildFunction(f))
|
||||
}
|
||||
|
||||
func writePartialsFile() {
|
||||
cwd, _ := os.Getwd()
|
||||
partialPath := filepath.Join(cwd, "partials")
|
||||
funcs, err := findPublicFuncsReturningHPartial(partialPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := NewCodeBuilder(nil)
|
||||
builder.AppendLine(`// Package partials THIS FILE IS GENERATED. DO NOT EDIT.`)
|
||||
builder.AppendLine("package partials")
|
||||
builder.AddImport("mhtml/h")
|
||||
builder.AddImport("github.com/gofiber/fiber/v2")
|
||||
|
||||
buildGetPartialFromContext(builder, funcs)
|
||||
|
||||
WriteFile(filepath.Join("partials", "generated.go"), func(content *ast.File) string {
|
||||
return builder.String()
|
||||
})
|
||||
}
|
||||
|
||||
func formatRoute(path string) string {
|
||||
path = strings.TrimSuffix(path, "index.go")
|
||||
path = strings.TrimSuffix(path, ".go")
|
||||
path = strings.TrimPrefix(path, "pages/")
|
||||
path = strings.ReplaceAll(path, "$", ":")
|
||||
path = strings.ReplaceAll(path, "_", "/")
|
||||
path = strings.ReplaceAll(path, ".", "/")
|
||||
if path == "" {
|
||||
return "/"
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if strings.HasSuffix(path, "/") {
|
||||
return path[:len(path)-1]
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func writePagesFile() {
|
||||
builder := NewCodeBuilder(nil)
|
||||
builder.AppendLine(`// Package pages THIS FILE IS GENERATED. DO NOT EDIT.`)
|
||||
builder.AppendLine("package pages")
|
||||
builder.AddImport("github.com/gofiber/fiber/v2")
|
||||
builder.AddImport("mhtml/h")
|
||||
|
||||
pages, _ := findPublicFuncsReturningHPage("pages")
|
||||
|
||||
for _, page := range pages {
|
||||
if page.Import != "" && page.Package != "pages" {
|
||||
builder.AddImport(page.Import)
|
||||
}
|
||||
}
|
||||
|
||||
fName := "RegisterPages"
|
||||
body := `
|
||||
`
|
||||
|
||||
for _, page := range pages {
|
||||
call := fmt.Sprintf("%s.%s", page.Package, page.FuncName)
|
||||
if page.Package == "pages" {
|
||||
call = page.FuncName
|
||||
}
|
||||
|
||||
body += fmt.Sprintf(`
|
||||
f.Get("%s", func(ctx *fiber.Ctx) error {
|
||||
return h.HtmlView(ctx, %s(ctx))
|
||||
})
|
||||
`, formatRoute(page.Path), call)
|
||||
}
|
||||
|
||||
f := Function{
|
||||
Name: fName,
|
||||
Parameters: []NameType{
|
||||
{Name: "f", Type: "*fiber.App"},
|
||||
},
|
||||
Body: body,
|
||||
}
|
||||
|
||||
builder.Append(builder.BuildFunction(f))
|
||||
|
||||
WriteFile("pages/generated.go", func(content *ast.File) string {
|
||||
return builder.String()
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
writePartialsFile()
|
||||
writePagesFile()
|
||||
}
|
||||
82
tooling/astgen/map.go
Normal file
82
tooling/astgen/map.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package main
|
||||
|
||||
// OrderedMap is a generic data structure that maintains the order of keys.
|
||||
type OrderedMap[K comparable, V any] struct {
|
||||
keys []K
|
||||
values map[K]V
|
||||
}
|
||||
|
||||
// Entries returns the key-value pairs in the order they were added.
|
||||
func (om *OrderedMap[K, V]) Entries() []struct {
|
||||
Key K
|
||||
Value V
|
||||
} {
|
||||
entries := make([]struct {
|
||||
Key K
|
||||
Value V
|
||||
}, len(om.keys))
|
||||
for i, key := range om.keys {
|
||||
entries[i] = struct {
|
||||
Key K
|
||||
Value V
|
||||
}{
|
||||
Key: key,
|
||||
Value: om.values[key],
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
// NewOrderedMap creates a new OrderedMap.
|
||||
func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] {
|
||||
return &OrderedMap[K, V]{
|
||||
keys: []K{},
|
||||
values: make(map[K]V),
|
||||
}
|
||||
}
|
||||
|
||||
// Set adds or updates a key-value pair in the OrderedMap.
|
||||
func (om *OrderedMap[K, V]) Set(key K, value V) {
|
||||
// Check if the key already exists
|
||||
if _, exists := om.values[key]; !exists {
|
||||
om.keys = append(om.keys, key) // Append key to the keys slice if it's a new key
|
||||
}
|
||||
om.values[key] = value
|
||||
}
|
||||
|
||||
// Get retrieves a value by key.
|
||||
func (om *OrderedMap[K, V]) Get(key K) (V, bool) {
|
||||
value, exists := om.values[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// Keys returns the keys in the order they were added.
|
||||
func (om *OrderedMap[K, V]) Keys() []K {
|
||||
return om.keys
|
||||
}
|
||||
|
||||
// Values returns the values in the order of their keys.
|
||||
func (om *OrderedMap[K, V]) Values() []V {
|
||||
values := make([]V, len(om.keys))
|
||||
for i, key := range om.keys {
|
||||
values[i] = om.values[key]
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
// Delete removes a key-value pair from the OrderedMap.
|
||||
func (om *OrderedMap[K, V]) Delete(key K) {
|
||||
if _, exists := om.values[key]; exists {
|
||||
// Remove the key from the map
|
||||
delete(om.values, key)
|
||||
|
||||
// Remove the key from the keys slice
|
||||
for i, k := range om.keys {
|
||||
if k == key {
|
||||
om.keys = append(om.keys[:i], om.keys[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
tooling/astgen/util.go
Normal file
22
tooling/astgen/util.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func PanicF(format string, args ...interface{}) {
|
||||
panic(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Unique[T any](slice []T, key func(item T) string) []T {
|
||||
var result []T
|
||||
seen := make(map[string]bool)
|
||||
for _, v := range slice {
|
||||
k := key(v)
|
||||
if _, ok := seen[k]; !ok {
|
||||
seen[k] = true
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
72
tooling/astgen/writer.go
Normal file
72
tooling/astgen/writer.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func WriteFile(path string, cb func(content *ast.File) string) {
|
||||
currentDir, err := os.Getwd()
|
||||
|
||||
path = filepath.Join(currentDir, path)
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
|
||||
os.MkdirAll(dir, 0755)
|
||||
|
||||
bytes, err := os.ReadFile(path)
|
||||
|
||||
if err != nil {
|
||||
_, err = os.Create(path)
|
||||
bytes = make([]byte, 0)
|
||||
if err != nil {
|
||||
PanicF("Failed to create file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a FileSet to manage source file positions
|
||||
fset := token.NewFileSet()
|
||||
|
||||
// Parse the file into an AST
|
||||
f, _ := parser.ParseFile(fset, path, nil, parser.AllErrors)
|
||||
if f == nil {
|
||||
f = &ast.File{
|
||||
Name: ast.NewIdent("replacemeplz"), // Set the package name
|
||||
Decls: []ast.Decl{}, // No declarations initially
|
||||
}
|
||||
}
|
||||
|
||||
bytes = []byte(cb(f))
|
||||
formatEnabled := true
|
||||
|
||||
if formatEnabled {
|
||||
bytes, err = format.Source(bytes)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to format source: %v\n\n%s", err)
|
||||
data := string(bytes)
|
||||
println(data)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Define the file path where you want to save the buffer
|
||||
|
||||
cmd := exec.Command("git", "add", path)
|
||||
err = cmd.Run()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to run git add: %v\n", err)
|
||||
}
|
||||
|
||||
// Save the buffer to a file
|
||||
err = os.WriteFile(path, bytes, 0644)
|
||||
if err != nil {
|
||||
PanicF("Failed to write buffer to file: %v", err)
|
||||
}
|
||||
}
|
||||
117
tooling/watch.go
Normal file
117
tooling/watch.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
once := false
|
||||
if len(os.Args) > 1 {
|
||||
once = os.Args[1] == "--once"
|
||||
}
|
||||
|
||||
command := ""
|
||||
for i, arg := range os.Args {
|
||||
if arg == "--command" {
|
||||
command = os.Args[i+1]
|
||||
}
|
||||
}
|
||||
|
||||
if command == "" {
|
||||
panic("command is required")
|
||||
}
|
||||
|
||||
if once {
|
||||
runCommand(command)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("Recovered from fatal error:", r)
|
||||
// You can log the error here or take other corrective actions
|
||||
}
|
||||
}()
|
||||
|
||||
runCommand(command)
|
||||
// Create new watcher.
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
// Start listening for events.
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(event.Name, "generated.go") {
|
||||
continue
|
||||
}
|
||||
|
||||
if event.Has(fsnotify.Write) {
|
||||
success := runCommand(command)
|
||||
if success {
|
||||
log.Println(fmt.Sprintf("file changed. code generation successful"))
|
||||
} else {
|
||||
log.Println(fmt.Sprintf("file changed. code generation failed"))
|
||||
}
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Println("error:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
pagesDir := filepath.Join(cwd, "pages")
|
||||
partialsDir := filepath.Join(cwd, "partials")
|
||||
|
||||
toWatch := []string{pagesDir, partialsDir}
|
||||
|
||||
for _, watch := range toWatch {
|
||||
err = watcher.Add(watch)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Block main goroutine forever.
|
||||
<-make(chan struct{})
|
||||
}
|
||||
|
||||
func runCommand(command string) bool {
|
||||
// Create a new command
|
||||
cmd := exec.Command("bash", "-c", command)
|
||||
|
||||
// Capture stdout and stderr in buffers
|
||||
var out bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
// Run the command
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
log.Println(fmt.Sprintf("error: %v", err))
|
||||
println(stderr.String())
|
||||
return false
|
||||
} else {
|
||||
println(out.String())
|
||||
return true
|
||||
}
|
||||
}
|
||||
47
ui/button.go
Normal file
47
ui/button.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"mhtml/h"
|
||||
)
|
||||
|
||||
type ButtonProps struct {
|
||||
Text string
|
||||
Target string
|
||||
Get string
|
||||
Class string
|
||||
Children *h.Node
|
||||
}
|
||||
|
||||
func PrimaryButton(props ButtonProps) *h.Node {
|
||||
props.Class = h.MergeClasses(props.Class, "border-blue-700 bg-blue-700 text-white")
|
||||
return Button(props)
|
||||
}
|
||||
|
||||
func SecondaryButton(props ButtonProps) *h.Node {
|
||||
props.Class = h.MergeClasses(props.Class, "border-gray-700 bg-gray-700 text-white")
|
||||
return Button(props)
|
||||
}
|
||||
|
||||
func Button(props ButtonProps) *h.Node {
|
||||
|
||||
text := h.P(props.Text)
|
||||
|
||||
button := h.Button(
|
||||
h.If(props.Children != nil, props.Children),
|
||||
h.Class("flex gap-1 items-center border p-4 rounded cursor-hover", props.Class),
|
||||
h.If(props.Get != "", h.Get(props.Get)),
|
||||
h.If(props.Target != "", h.Target(props.Target)),
|
||||
//h.BeforeRequestSetHtml(
|
||||
// h.Div(
|
||||
// h.Class("flex gap-1"),
|
||||
// h.Text("Loading..."),
|
||||
// ),
|
||||
//),
|
||||
//h.AfterRequestSetHtml(h.Text(props.Text)),
|
||||
// Note, i really like this idea of being able to reference elements just by the instance,
|
||||
//and automatically adding id
|
||||
text,
|
||||
)
|
||||
|
||||
return button
|
||||
}
|
||||
Loading…
Reference in a new issue