Compare commits

...

474 commits

Author SHA1 Message Date
maddalax
72c171709e wrap example in div 2025-08-16 09:47:51 -05:00
maddalax
5dba9d0167 param -> urlParam 2025-08-15 09:05:46 -05:00
github-actions[bot]
6f29b307ec Auto-update HTMGO framework version 2025-07-03 19:08:02 +00:00
Eliah Rusin
06f01b3d7c
Refactor caching system to use pluggable stores (#98)
* Refactor caching system to use pluggable stores

The commit modernizes the caching implementation by introducing a pluggable store interface that allows different cache backends. Key changes:

- Add Store interface for custom cache implementations
- Create default TTL-based store for backwards compatibility
- Add example LRU store for memory-bounded caching
- Support cache store configuration via options pattern
- Make cache cleanup logic implementation-specific
- Add comprehensive tests and documentation

The main goals were to:

1. Prevent unbounded memory growth through pluggable stores
2. Enable distributed caching support
3. Maintain backwards compatibility
4. Improve testability and maintainability

Signed-off-by: franchb <hello@franchb.com>

* Add custom cache stores docs and navigation

Signed-off-by: franchb <hello@franchb.com>

* Use GetOrCompute for atomic cache access

The commit introduces an atomic GetOrCompute method to the cache interface and refactors all cache implementations to use it. This prevents race conditions and duplicate computations when multiple goroutines request the same uncached key simultaneously.

The changes eliminate a time-of-check to time-of-use race condition in the original caching implementation, where separate Get/Set operations could lead to duplicate renders under high concurrency.

With GetOrCompute, the entire check-compute-store operation happens atomically while holding the lock, ensuring only one goroutine computes a value for any given key.

The API change is backwards compatible as the framework handles the GetOrCompute logic internally. Existing applications will automatically benefit from the

* rename to WithCacheStore

---------

Signed-off-by: franchb <hello@franchb.com>
Co-authored-by: maddalax <jm@madev.me>
2025-07-03 14:07:16 -05:00
maddalax
d555e5337f make run server build the binary instead of outputting each run to a tmp file
ensure tailwind cli is v3 for now
2025-01-24 11:51:01 -06:00
maddalax
c406b5f068 Revert "Revert "make run server build the binary instead of outputting each run to a tmp file""
This reverts commit c52f10f92d.
2025-01-24 11:50:44 -06:00
maddalax
c52f10f92d Revert "make run server build the binary instead of outputting each run to a tmp file"
This reverts commit d9c7fb3936.
2025-01-24 11:39:22 -06:00
maddalax
d9c7fb3936 make run server build the binary instead of outputting each run to a tmp file 2025-01-24 11:36:53 -06:00
github-actions[bot]
66b6dfffd3 Auto-update HTMGO framework version 2025-01-06 16:27:11 +00:00
maddalax
24b41a7604 Merge remote-tracking branch 'origin/master' 2025-01-06 10:26:19 -06:00
maddalax
0c84e42160 add info on how to change it 2025-01-06 10:26:15 -06:00
github-actions[bot]
ca4faf103e Auto-update HTMGO framework version 2025-01-06 16:25:41 +00:00
maddalax
4f537567ad allow port to be configured 2025-01-06 10:24:49 -06:00
maddalax
0d61b12561 fix snippet 2024-11-30 10:57:42 -06:00
maddalax
3f719d7011 remove new lines 2024-11-28 10:13:23 -06:00
maddalax
331f4cde82 test auto deploy 2024-11-25 11:03:40 -06:00
maddalax
ab50eaecf4 mobile fixes css 2024-11-25 10:34:22 -06:00
maddalax
baf10419f7 fix examples link 2024-11-25 10:23:12 -06:00
maddalax
c924f63ffb test 2024-11-24 10:11:41 -06:00
maddalax
ab342535d3 test 2024-11-24 10:08:41 -06:00
maddalax
15655d5c02 Merge remote-tracking branch 'origin/master' 2024-11-23 02:08:15 -06:00
maddalax
14272d6507 test 2024-11-23 02:08:11 -06:00
maddalax
d325677a1f
Update README.md 2024-11-21 07:54:23 -06:00
maddalax
e0bb30b976 remove assets dir 2024-11-21 07:51:49 -06:00
maddalax
baf5292212 add js 2024-11-21 07:51:08 -06:00
maddalax
9b69b25d0b minimal htmgo doc 2024-11-21 07:45:34 -06:00
maddalax
495e759689 remove from pages as its not needed 2024-11-21 07:29:51 -06:00
maddalax
ba8c0106d9 Merge remote-tracking branch 'origin/master' 2024-11-21 07:29:10 -06:00
maddalax
4c942a0a16
Minimal htmgo (#84)
* add RunAfterTimeout & RunOnInterval

* minimal htmgo
2024-11-21 07:27:22 -06:00
maddalax
407cc12079 Merge remote-tracking branch 'origin/master' 2024-11-21 07:04:32 -06:00
maddalax
01e4568c48
Update README.md 2024-11-18 18:14:51 -06:00
maddalax
158a6264a9 Merge remote-tracking branch 'origin/master' 2024-11-18 11:28:40 -06:00
github-actions[bot]
909d38c7f4 Auto-update HTMGO framework version 2024-11-16 14:52:58 +00:00
maddalax
825c4dd7ec
add RunAfterTimeout & RunOnInterval (#75) 2024-11-16 08:52:00 -06:00
maddalax
ef83e34b1e add RunAfterTimeout & RunOnInterval 2024-11-16 08:45:03 -06:00
Rafael de Mattos
a1af01a480
Check project directories (#70)
* Check project directories

* Remove partials directory

---------

Co-authored-by: maddalax <jm@madev.me>
2024-11-14 09:54:53 -06:00
maddalax
971f05c005 Revert "move socket manager"
This reverts commit 423fd3f429.
2024-11-12 18:16:20 -06:00
maddalax
b06d1b14bd log http requests 2024-11-12 18:15:59 -06:00
maddalax
423fd3f429 move socket manager 2024-11-12 13:04:20 -06:00
maddalax
257def3b53 up cli version 2024-11-12 08:55:35 -06:00
maddalax
97a5687f2e astgen only 2024-11-11 10:12:29 -06:00
maddalax
d2d8e449ae does this work 2024-11-11 10:10:40 -06:00
maddalax
a2d3a367d1 move codecov 2024-11-11 10:01:11 -06:00
maddalax
dc8a62313c only generate routes for partials or pages that have *h.RequestContext as a param 2024-11-11 09:55:09 -06:00
maddalax
6ec582a834
allow partials throughout the project, not just partials folder (#72)
* allow partials throughout the project, not jsut partials file

* route directly to partial

* generate correctly even if there is no partials

* run cli tests

* tidy

* only run tests on master if push

* add codecov
2024-11-11 09:17:57 -06:00
github-actions[bot]
b3834bf559 Auto-update HTMGO framework version 2024-11-09 18:33:21 +00:00
maddalax
b234ead964 fix loading livereload extension 2024-11-09 12:32:30 -06:00
github-actions[bot]
a756a0484f Auto-update HTMGO framework version 2024-11-09 18:06:38 +00:00
maddalax
34e816ff7c
Websocket Extension - Alpha (#22)
* wip

* merge

* working again

* refactor/make it a bit cleaner

* fix to only call cb for session id who initiated the event

* support broadcasting events to all clients

* refactor

* refactor into ws extension

* add go mod

* rename module

* fix naming

* refactor

* rename

* merge

* fix manager ws delete, add manager tests

* add metric page

* fixes, add k6 script

* fixes, add k6 script

* deploy docker image

* cleanup

* cleanup

* cleanup
2024-11-09 12:05:53 -06:00
maddalax
841262341a test 2024-11-05 15:17:12 -06:00
maddalax
142411c0e5 test 2024-11-05 15:13:05 -06:00
maddalax
e424dac826 test 2024-11-05 15:09:22 -06:00
maddalax
6acfc74a65 version 2024-11-01 07:57:30 -05:00
maddalax
aeb3a7be64 fixes 2024-11-01 07:53:48 -05:00
maddalax
ea997b41de debug 2024-11-01 07:44:17 -05:00
maddalax
7d04d8861f add windows instructions 2024-11-01 07:29:15 -05:00
maddalax
bf9cf2bf96 add version 2024-11-01 07:23:18 -05:00
maddalax
2346708ab1 windows fix 2024-11-01 07:09:58 -05:00
maddalax
25c216e2b6 mod tidy 2024-11-01 06:16:29 -05:00
github-actions[bot]
af0091c370 Auto-update HTMGO framework version 2024-11-01 11:11:25 +00:00
maddalax
2c4ac8b286
gen code for assets (#68)
* gen code for assets

* fix

* test
2024-11-01 06:10:35 -05:00
maddalax
4c6187e18d add docs for eval commands 2024-11-01 05:08:36 -05:00
maddalax
b75dadf00e add prefix 2024-10-31 18:19:32 -05:00
maddalax
e067a17f53 add more urls to sitemap 2024-10-31 18:10:11 -05:00
maddalax
9643e08232 Merge remote-tracking branch 'origin/master' 2024-10-31 13:55:31 -05:00
maddalax
06792019f8 add discord link 2024-10-31 13:55:27 -05:00
maddalax
f952d6ed3e
Update README.md 2024-10-31 13:41:13 -05:00
maddalax
f0b8118e00 Merge remote-tracking branch 'origin/master' 2024-10-31 13:40:47 -05:00
maddalax
e27cda5779 add discord route 2024-10-31 13:40:43 -05:00
maddalax
f25c6bd8f5
Update README.md 2024-10-31 13:36:09 -05:00
github-actions[bot]
64f201f4a3 Auto-update HTMGO framework version 2024-10-31 17:00:12 +00:00
maddalax
032159149c Merge remote-tracking branch 'origin/master' 2024-10-31 11:59:23 -05:00
maddalax
2d6ab078be swap tests 2024-10-31 11:59:19 -05:00
github-actions[bot]
479df08d63 Auto-update HTMGO framework version 2024-10-31 16:44:28 +00:00
maddalax
0a4bcfa7a3 Merge remote-tracking branch 'origin/master' 2024-10-31 11:43:28 -05:00
maddalax
248e485ff0 ordered map tests, extensions test 2024-10-31 11:43:24 -05:00
maddalax
51995681e3
Update README.md 2024-10-31 11:39:21 -05:00
github-actions[bot]
92f33c8cff Auto-update HTMGO framework version 2024-10-31 16:37:49 +00:00
maddalax
e268a581ce Merge remote-tracking branch 'origin/master' 2024-10-31 11:37:01 -05:00
maddalax
f42351e94f more tests 2024-10-31 11:36:57 -05:00
github-actions[bot]
9ed353ebe3 Auto-update HTMGO framework version 2024-10-31 16:12:09 +00:00
maddalax
8a00828232 conditional tests 2024-10-31 11:11:09 -05:00
maddalax
01cde545ce Merge remote-tracking branch 'origin/master' 2024-10-31 11:01:44 -05:00
maddalax
6db280907a upload to codecov 2024-10-31 11:01:40 -05:00
maddalax
ca946f2cba
Update README.md 2024-10-31 10:55:17 -05:00
maddalax
7da172cfad
Update README.md 2024-10-31 10:53:32 -05:00
maddalax
de0e06155b text input example 2024-10-31 10:16:03 -05:00
maddalax
44461b1ec7 add helper 2024-10-31 09:58:16 -05:00
maddalax
833708e38a wrap lines on examples 2024-10-31 09:56:37 -05:00
github-actions[bot]
7f5274b34d Auto-update HTMGO framework version 2024-10-31 14:45:06 +00:00
maddalax
f6556b579f css form on blur validation 2024-10-31 09:44:16 -05:00
maddalax
3cd7577b06 css fixes 2024-10-30 14:44:02 -05:00
maddalax
e6223a36b7 remove myapp 2024-10-30 14:32:27 -05:00
maddalax
60a37b65d6 change version 2024-10-30 14:17:35 -05:00
maddalax
5feb271aed fix 2024-10-30 14:14:12 -05:00
maddalax
cb6fcdd676 new favicon 2024-10-30 14:07:38 -05:00
maddalax
129c230c72 fix link 2024-10-30 13:50:52 -05:00
maddalax
965a8487b9 add title tag to starter template 2024-10-30 13:40:49 -05:00
maddalax
1ce5f37fc4 set title 2024-10-30 13:39:07 -05:00
maddalax
2e60b84c83 Merge remote-tracking branch 'origin/master' 2024-10-30 13:29:24 -05:00
maddalax
61b215436b cleanup 2024-10-30 13:29:20 -05:00
github-actions[bot]
101bb022c9 Auto-update HTMGO framework version 2024-10-30 18:28:28 +00:00
maddalax
35877a1b2e
New Docs (#63)
* scripting enhancements

* tests

* cleanup / tests

* new docs wip

* add more docs

* more updates

* add caching docs

* add sse docs

* more docs

* sidebar, and fix navigation blocks

* remove old docs

* set proper meta

* fixes
2024-10-30 13:27:42 -05:00
github-actions[bot]
df9c7f9cf7 Auto-update HTMGO framework version 2024-10-29 13:45:48 +00:00
maddalax
d85737bfb8
JS Eval Enhancements (#62)
* scripting enhancements

* tests

* cleanup / tests
2024-10-29 08:44:52 -05:00
maddalax
10f48af304 Merge remote-tracking branch 'origin/master' 2024-10-29 05:54:38 -05:00
maddalax
2ec9fd14c0 remove border for iframe examples 2024-10-29 05:54:34 -05:00
github-actions[bot]
0c22fc137b Auto-update HTMGO framework version 2024-10-29 10:49:14 +00:00
maddalax
cb012a4d82 cleanup usage of orderedmap
add tests
add groupby
add groupbyordered
2024-10-29 05:48:13 -05:00
maddalax
d44cd0b2ed fix urls 2024-10-29 05:11:35 -05:00
maddalax
60e1a161ca simplify since we arent allowing editing on load 2024-10-28 18:55:42 -05:00
maddalax
1fd773f726 Merge remote-tracking branch 'origin/master' 2024-10-28 18:50:35 -05:00
maddalax
2e110dcafd title case 2024-10-28 18:50:31 -05:00
github-actions[bot]
59a75aac9d Auto-update HTMGO framework version 2024-10-28 23:47:56 +00:00
maddalax
7666186f83 add inline edit / fix copy button 2024-10-28 18:47:00 -05:00
maddalax
db3322c3d8 fix process kill on windows
normalize paths
2024-10-28 14:02:17 -05:00
maddalax
9dac3ef988 ensure the pattern matches too 2024-10-28 12:52:21 -05:00
maddalax
044cbbe698 path matching on windows 2024-10-28 12:46:33 -05:00
maddalax
62a141ff6c blur overlay on iframe 2024-10-28 11:49:21 -05:00
maddalax
930b3dd1d3 replace examples page with v2 2024-10-28 11:39:46 -05:00
maddalax
1ca8bbf6e0 render the code from github since we wont have it locally 2024-10-28 11:27:25 -05:00
github-actions[bot]
f1f173715e Auto-update HTMGO framework version 2024-10-28 15:56:54 +00:00
maddalax
b65a913d4e test more examples 2024-10-28 10:56:03 -05:00
maddalax
349d07c691 Merge remote-tracking branch 'origin/master' 2024-10-28 10:32:55 -05:00
maddalax
42febeb654 snippets view testing / cleanup 2024-10-28 10:32:50 -05:00
github-actions[bot]
864d2ed3ba Auto-update HTMGO framework version 2024-10-27 02:42:14 +00:00
maddalax
2726c60608 set charset on html content type 2024-10-26 21:41:21 -05:00
maddalax
b218a75ea8 add related projects 2024-10-26 17:08:55 -05:00
maddalax
0895f74b13 Merge remote-tracking branch 'origin/master' 2024-10-26 16:55:19 -05:00
maddalax
4693820820 more css fixes 2024-10-26 16:55:15 -05:00
maddalax
0c6a8d7d15
Update README.md 2024-10-26 16:45:47 -05:00
maddalax
62599c22ea some more fixes 2024-10-26 15:26:23 -05:00
maddalax
5ee5d956f0 fix max width 2024-10-26 15:15:31 -05:00
maddalax
8feb717d91 quick mobile fix 2024-10-26 14:56:51 -05:00
maddalax
db0888453a sanitize at the data layer too 2024-10-26 09:38:57 -05:00
maddalax
7b10b20b9e santize 2024-10-26 09:32:26 -05:00
maddalax
613685aebb Merge remote-tracking branch 'origin/master' 2024-10-26 09:14:37 -05:00
maddalax
3d4f3a9a77 remove unsafe 2024-10-26 09:14:33 -05:00
github-actions[bot]
816cb679c7 Auto-update HTMGO framework version 2024-10-26 03:07:52 +00:00
maddalax
8e048cbddc Merge remote-tracking branch 'origin/master' 2024-10-25 22:07:03 -05:00
maddalax
2f35c08afd comments about the service locator 2024-10-25 22:06:58 -05:00
github-actions[bot]
64b0435c86 Auto-update HTMGO framework version 2024-10-26 03:02:02 +00:00
maddalax
3468baaa84 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	framework/h/attribute.go
#	framework/h/lifecycle.go
#	framework/h/render.go
2024-10-25 22:01:04 -05:00
maddalax
cf76ca4f98 add some comments 2024-10-25 21:59:17 -05:00
maddalax
4398eb1fdb style fixes 2024-10-25 14:11:48 -05:00
maddalax
48968bac78 Merge remote-tracking branch 'origin/master' 2024-10-25 13:33:59 -05:00
maddalax
e3edd49800 add logo 2024-10-25 13:33:55 -05:00
github-actions[bot]
abad4b4eb7 Auto-update HTMGO framework version 2024-10-25 17:42:22 +00:00
Mohammad javad
df3edccd7f
fix:parser_test remove impossible condition (#44) 2024-10-25 12:41:32 -05:00
maddalax
5a69d9ba20 cleanup the intro example 2024-10-25 12:39:22 -05:00
maddalax
a2be0e2fca Merge remote-tracking branch 'origin/master' 2024-10-25 11:30:54 -05:00
maddalax
97ad3cbee2 add top banner 2024-10-25 11:30:50 -05:00
github-actions[bot]
2953a487d2 Auto-update HTMGO framework version 2024-10-25 16:12:08 +00:00
maddalax
e2939cd8ba support non pointer types on formatter 2024-10-25 11:11:22 -05:00
maddalax
7fae44790c Merge remote-tracking branch 'origin/master' 2024-10-25 10:54:16 -05:00
maddalax
504a64aa63 add instructions for running the formatter 2024-10-25 10:54:12 -05:00
github-actions[bot]
f4083b7014 Auto-update HTMGO framework version 2024-10-25 15:34:32 +00:00
maddalax
8736c00fd5
htmgo - custom formatter (#47)
* format htmgo elements on save

* formatter updates

* ensure we maintain comments
2024-10-25 10:33:48 -05:00
maddalax
3f8ab7d905 add copy button 2024-10-25 08:12:11 -05:00
github-actions[bot]
caa9d10b1b Auto-update HTMGO framework version 2024-10-25 12:52:22 +00:00
maddalax
9a87188318 run if tool changes 2024-10-25 07:51:11 -05:00
maddalax
1924270e4e update more deps 2024-10-25 07:48:41 -05:00
maddalax
d538f6f7a8 upgrade 2024-10-25 07:15:11 -05:00
maddalax
f9e5d0eb2e Merge remote-tracking branch 'origin/master'
# Conflicts:
#	htmgo-site/go.mod
2024-10-25 07:15:00 -05:00
maddalax
5a2b0f4f4b upgrade 2024-10-25 07:14:37 -05:00
github-actions[bot]
e496a41a39 Auto-update HTMGO framework version 2024-10-25 12:08:14 +00:00
maddalax
61758622ef indent -> indentme
add details & summary html tag
2024-10-25 07:07:35 -05:00
maddalax
531ad3342b much better formatting for the generated go code 2024-10-25 07:05:12 -05:00
maddalax
df490ae04b copy assets on run if needed 2024-10-24 08:51:06 -05:00
maddalax
7af750b9e7 Revert "check in go.work"
This reverts commit 90fc6e8726.
2024-10-24 08:41:36 -05:00
maddalax
90fc6e8726 check in go.work 2024-10-24 08:39:19 -05:00
jam
4dd501a2f8
fix: filter by filename instead of whole path (#42) 2024-10-24 08:37:01 -05:00
github-actions[bot]
64c61dd178 Auto-update HTMGO framework version 2024-10-23 16:11:29 +00:00
maddalax
5d4efc76de Merge remote-tracking branch 'origin/master' 2024-10-23 11:10:52 -05:00
maddalax
21ac153d5b add transient to service loader, clear cache when setting a new value for the service 2024-10-23 11:10:48 -05:00
github-actions[bot]
fa8f843154 Auto-update HTMGO framework version 2024-10-23 15:51:03 +00:00
maddalax
a72de0a62f add qs tests 2024-10-23 10:50:22 -05:00
maddalax
f3db495bd2 Merge remote-tracking branch 'origin/master' 2024-10-23 10:35:03 -05:00
maddalax
07b0c2d3c5 fix css on html to go 2024-10-23 10:34:59 -05:00
github-actions[bot]
31cf8772c2 Auto-update HTMGO framework version 2024-10-23 14:29:05 +00:00
maddalax
bb9fb0b327 write doctype when rendering html 2024-10-23 09:28:19 -05:00
github-actions[bot]
146034c78b Auto-update HTMGO framework version 2024-10-22 13:49:54 +00:00
Mohammad javad
30ac29fd6c
use path (#32)
* feat:add claasf

* refactor(tag.go)

* refactor(cache.go)

* refactor(attribute.go)

* refactor(app.go)

* refactor(attribute.go)

* fix:use path in PostPartialWithQs

* fix: PostPartial
2024-10-22 08:49:17 -05:00
github-actions[bot]
4eabe21d68 Auto-update HTMGO framework version 2024-10-22 13:32:58 +00:00
Mohammad javad
a0f5b5dfd5
feat:add claasf (#31)
* feat:add claasf

* refactor(tag.go)

* refactor(cache.go)

* refactor(attribute.go)

* refactor(app.go)

* refactor(attribute.go)
2024-10-22 08:32:17 -05:00
maddalax
70228912be remove chat deploy on pr 2024-10-22 08:17:58 -05:00
maddalax
b92213b356 oops 2024-10-21 10:38:14 -05:00
maddalax
86e293498f update landing page 2024-10-21 10:35:42 -05:00
maddalax
2c4762142e Merge remote-tracking branch 'origin/master' 2024-10-21 10:23:45 -05:00
maddalax
14911799b7 add config docs 2024-10-21 10:21:52 -05:00
github-actions[bot]
ed2063df17 Auto-update HTMGO framework version 2024-10-21 15:07:08 +00:00
maddalax
635b17dd7f add support for ignoring specific files for automatic page/partial routing 2024-10-21 10:06:18 -05:00
github-actions[bot]
245d63c7b2 Auto-update HTMGO framework version 2024-10-21 14:18:00 +00:00
Kevin Lewin
cea6b1cff2
Update slog output for server start (#27)
Fixing f string error.
2024-10-21 09:17:18 -05:00
github-actions[bot]
7a038e6af1 Auto-update HTMGO framework version 2024-10-20 15:22:16 +00:00
maddalax
b6d901fadf add some helper methods for redirect and setting cookie 2024-10-20 10:21:37 -05:00
maddalax
c32fa1bccd add user auth to examples page 2024-10-20 08:09:40 -05:00
maddalax
b8d23131b7 fix entrypoint 2024-10-20 08:03:30 -05:00
maddalax
46b21f75b5 fix build 2024-10-20 08:01:24 -05:00
maddalax
19b700b676 small fix 2024-10-20 07:56:26 -05:00
maddalax
c6c05193c6 fix dockerfile 2024-10-20 07:55:18 -05:00
maddalax
39744d6248 add workflow 2024-10-20 07:52:13 -05:00
maddalax
72d1680946 remove log 2024-10-20 07:51:18 -05:00
maddalax
abfc3b0205 remove db 2024-10-20 07:49:24 -05:00
maddalax
19638326dd simple auth example 2024-10-20 07:48:58 -05:00
maddalax
13f650b28b simple auth example 2024-10-20 07:48:19 -05:00
github-actions[bot]
1d7ac301ab Auto-update HTMGO framework version 2024-10-18 22:30:40 +00:00
Kevin Lewin
a7110576d2
Update app.go (#24)
Reformat server listening log message so user could click from terminal or ide
2024-10-18 17:29:59 -05:00
github-actions[bot]
0ccd236307 Auto-update HTMGO framework version 2024-10-17 15:10:56 +00:00
maddalax
2f0a526245 use non dev version of htmgo.js 2024-10-17 10:10:08 -05:00
maddalax
69698b312d
Update README.md 2024-10-15 11:25:06 -05:00
maddalax
907cf86f6a add quick sitemap 2024-10-15 11:15:47 -05:00
maddalax
a466726b70 send insights 2024-10-15 10:51:32 -05:00
maddalax
3621428093 make docs a link 2024-10-15 10:43:37 -05:00
maddalax
55bfb4d997 small fix 2024-10-15 10:40:13 -05:00
maddalax
f62d47e2fb fix formatting on docs 2024-10-15 10:37:25 -05:00
maddalax
0ada050eff add docsearch 2024-10-15 10:26:30 -05:00
github-actions[bot]
82382cf43f Auto-update HTMGO framework version 2024-10-14 15:17:42 +00:00
maddalax
8503dffa4e
Merge pull request #21 from maddalax/config-wip
htmgo yml config
2024-10-14 10:17:03 -05:00
maddalax
aff426829d fix tests 2024-10-14 10:14:22 -05:00
maddalax
55337cde13 use diff dir 2024-10-14 10:06:00 -05:00
maddalax
c3be0e92d2 few tests 2024-10-14 10:05:14 -05:00
maddalax
d21afa5f6d few tests 2024-10-14 10:02:04 -05:00
maddalax
3f502dba81 cleanup / add err handling 2024-10-14 09:58:37 -05:00
maddalax
49f4067bc5 add default config file to template 2024-10-14 09:52:06 -05:00
maddalax
2bac9307c6 more config 2024-10-14 09:49:48 -05:00
maddalax
7225b1908b
Update README.md 2024-10-14 08:26:20 -05:00
maddalax
da82b7f536 config wip 2024-10-13 16:33:08 -05:00
maddalax
289337b5e4 missed 2024-10-13 08:27:56 -05:00
maddalax
f39c4d2c58 fix doc page css 2024-10-13 08:24:23 -05:00
maddalax
149f64ebb6 Merge remote-tracking branch 'origin/master' 2024-10-13 06:35:32 -05:00
maddalax
186bb7d14c add tailwind intellisense notes 2024-10-13 06:35:27 -05:00
maddalax
56e76f88d8
Create FUNDING.yml 2024-10-11 23:43:27 -05:00
maddalax
a414a5b3a9
Create CODE_OF_CONDUCT.md 2024-10-11 23:33:23 -05:00
maddalax
72e3383e75 fix script tag 2024-10-11 11:39:47 -05:00
maddalax
80d3538d87 update docs 2024-10-11 11:32:32 -05:00
maddalax
2476b9a106 update version, add to nav 2024-10-11 11:24:54 -05:00
maddalax
58184cd726 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	htmgo-site/go.mod
#	htmgo-site/go.sum
2024-10-11 11:24:39 -05:00
maddalax
77ff5dbbd0 update version, add to nav 2024-10-11 11:21:53 -05:00
github-actions[bot]
8ed4121f38 Auto-update HTMGO framework version 2024-10-11 16:20:24 +00:00
maddalax
8b9e536f14 add converter to site 2024-10-11 11:19:32 -05:00
maddalax
980afbc5ac fix imports 2024-10-11 10:28:34 -05:00
maddalax
8e4a63d224 add html-to-htmgo tool 2024-10-11 10:25:41 -05:00
github-actions[bot]
7d748ec7e6 Auto-update HTMGO framework version 2024-10-11 14:12:20 +00:00
maddalax
d0c272e1e5 Merge remote-tracking branch 'origin/master' 2024-10-11 09:11:33 -05:00
maddalax
e750315764 release site after deps updated 2024-10-11 09:11:28 -05:00
github-actions[bot]
4a2a119ef1 Auto-update HTMGO framework version 2024-10-11 14:08:48 +00:00
maddalax
8799772634 Merge remote-tracking branch 'origin/master' 2024-10-11 09:08:01 -05:00
maddalax
90b444a097 test 2024-10-11 09:07:56 -05:00
github-actions[bot]
21652658c3 Auto-update HTMGO framework version 2024-10-11 13:57:53 +00:00
maddalax
ea5d4d5e2e update onload to always be called when element gets rendered again 2024-10-11 08:56:56 -05:00
maddalax
9f9f43751f handle /item redirect 2024-10-10 21:01:39 -05:00
maddalax
a49346bda4 add top banner 2024-10-10 20:37:24 -05:00
maddalax
0c0a018cd2 Merge remote-tracking branch 'origin/master' 2024-10-10 20:31:24 -05:00
maddalax
5c87d96a51 css fixes 2024-10-10 20:31:20 -05:00
github-actions[bot]
775d012079 Auto-update HTMGO framework version 2024-10-11 01:18:18 +00:00
maddalax
819af8b7b8 infinite scroll 2024-10-10 20:17:31 -05:00
maddalax
bd344d56ef update meta 2024-10-10 18:01:51 -05:00
maddalax
c2b3b439e6 add comments loader, cache stories 2024-10-10 17:59:08 -05:00
maddalax
78f7a68c1e add hn clone to examples 2024-10-10 17:31:07 -05:00
maddalax
2e998dbb26 fixes 2024-10-10 17:17:05 -05:00
maddalax
e0344787f1 Merge remote-tracking branch 'origin/master' 2024-10-10 17:01:42 -05:00
maddalax
49aae062ef hn clone 2024-10-10 17:01:38 -05:00
github-actions[bot]
cb6619594a Auto-update HTMGO framework version 2024-10-10 22:01:07 +00:00
maddalax
4880946515 hn clone 2024-10-10 17:00:20 -05:00
maddalax
c0fabcedd2 no sticky sidebar for now because of small screens 2024-10-10 10:13:57 -05:00
maddalax
e860f715d8 add ideas about converting html to htmgo code 2024-10-10 09:57:37 -05:00
maddalax
7fd213ccab fix taskfile 2024-10-10 09:30:37 -05:00
maddalax
b8dbcd4262 add favicon / public folder to template and htmgo-site 2024-10-10 09:24:50 -05:00
maddalax
064fea3a00 add note about combining ext 2024-10-10 09:13:27 -05:00
maddalax
1c3065e432 add info about the extensions 2024-10-10 09:07:03 -05:00
maddalax
3fb719bf15 removing sorting 2024-10-10 08:35:04 -05:00
github-actions[bot]
cb9bb23e2d Auto-update HTMGO framework version 2024-10-09 15:37:57 +00:00
maddalax
95f9b43395
Merge pull request #18 from maddalax/fixes-2
Various fixes
2024-10-09 10:37:12 -05:00
maddalax
298f4dcd61 revert 2024-10-09 10:31:34 -05:00
maddalax
0b38bcaa21 Revert "websocket stuff wip"
This reverts commit 9f53e8b2
2024-10-09 10:30:14 -05:00
maddalax
f3cb95960c fix watcher to watch newly added dirs
add utility methods to RequestContext

Fix GetPartialPath
2024-10-09 10:28:41 -05:00
maddalax
c4b1df8a6d Merge remote-tracking branch 'origin/master' 2024-10-09 09:58:03 -05:00
maddalax
9f53e8b2aa websocket stuff wip 2024-10-08 12:48:28 -05:00
github-actions[bot]
b353aa90f4 Auto-update HTMGO framework version 2024-10-07 17:58:32 +00:00
maddalax
1014f6c961 add htmx to window
small swap fix
copy public assets to dist if changed
2024-10-07 12:57:24 -05:00
github-actions[bot]
5cf90208d8 Auto-update HTMGO framework version 2024-10-06 16:22:21 +00:00
maddalax
150c87b456 fix trigger children ext 2024-10-06 11:21:37 -05:00
maddalax
1f0a840396
Update README.md 2024-10-06 10:18:29 -05:00
maddalax
e019c288de fix 2024-10-04 11:52:54 -05:00
maddalax
1e39e39a04 Merge remote-tracking branch 'origin/master' 2024-10-04 11:22:51 -05:00
maddalax
60651361e2 revert 2024-10-04 11:22:48 -05:00
github-actions[bot]
d11044f638 Auto-update HTMGO framework version 2024-10-04 16:21:55 +00:00
maddalax
805e5729e1 build fix 2024-10-04 11:21:15 -05:00
maddalax
a7e1311ad5 Merge remote-tracking branch 'origin/master' 2024-10-04 11:18:36 -05:00
maddalax
697b4c4924 undo 2024-10-04 11:18:33 -05:00
github-actions[bot]
7d9857f97e Auto-update HTMGO framework version 2024-10-04 16:16:51 +00:00
maddalax
1e51b6b8e8 some cleanup 2024-10-04 11:15:57 -05:00
maddalax
7503e1b2c1 add docs for SSE 2024-10-04 11:12:38 -05:00
maddalax
e235aa58ba Merge remote-tracking branch 'origin/master' 2024-10-04 10:38:19 -05:00
maddalax
f65c5b303e add chat example 2024-10-04 10:38:15 -05:00
github-actions[bot]
2b24b8adff Auto-update HTMGO framework version 2024-10-04 15:33:55 +00:00
maddalax
caad5633d0
Merge pull request #13 from maddalax/chat-app
Chat App Example
2024-10-04 10:33:14 -05:00
maddalax
9258f3ed06 fixes 2024-10-04 10:33:04 -05:00
maddalax
e6af7a73d5 fix test 2024-10-03 15:41:09 -05:00
maddalax
496981efd0 fix tests 2024-10-03 15:35:19 -05:00
maddalax
a06f5fea12 css fixes 2024-10-03 12:04:49 -05:00
maddalax
aa6d311fa8 sidebar fixes 2024-10-03 11:38:40 -05:00
maddalax
deb87dceed test 2024-10-02 12:56:11 -05:00
maddalax
6ea3f77b62 fix issues with channel buffer size 2024-10-02 12:10:04 -05:00
maddalax
d739ef3758 fix issues with error handling 2024-10-02 11:06:00 -05:00
maddalax
33b4b3299e fixes with managing the sse connections 2024-10-02 10:48:41 -05:00
maddalax
5b60b9e915 fixes for error handling 2024-10-01 23:04:04 -05:00
maddalax
f06feffb9e forgot prod build tag 2024-10-01 22:39:30 -05:00
maddalax
ecb42207f1 some fixes 2024-10-01 22:31:41 -05:00
maddalax
8b816e9566 switch to sse 2024-10-01 22:26:03 -05:00
maddalax
5b10aed601 cleanup 2024-10-01 17:19:38 -05:00
maddalax
d37bbd85a5 cache fix 2024-10-01 14:02:32 -05:00
maddalax
5089dea7a1 fix build 2024-10-01 13:52:26 -05:00
maddalax
9a5b929877 add gh 2024-10-01 13:45:32 -05:00
maddalax
48946810be add gh 2024-10-01 13:43:05 -05:00
maddalax
25f12aa49e some fixes 2024-10-01 13:42:14 -05:00
maddalax
f4f64fefcc some styling 2024-10-01 12:56:16 -05:00
maddalax
8abed86b7d some styling and validation 2024-10-01 12:42:01 -05:00
maddalax
784995728c add error handling 2024-10-01 12:09:22 -05:00
maddalax
5233bbb234 chat semi working 2024-09-30 22:49:03 -05:00
maddalax
27b5de93ea chat working 2024-09-30 22:08:52 -05:00
maddalax
33063f992a fix dynamic paths for chi 2024-09-30 20:34:15 -05:00
maddalax
787ccb4fc1 entry page 2024-09-30 20:32:42 -05:00
maddalax
ae983473b3 db progress 2024-09-30 17:31:09 -05:00
maddalax
8cdc625133 some cleanup 2024-09-30 16:32:12 -05:00
maddalax
c7f4781137 spin up chat app, setup sockets, fix trigger children to work 2024-09-30 16:05:06 -05:00
github-actions[bot]
d2072fe777 Auto-update HTMGO framework version 2024-09-30 18:04:58 +00:00
maddalax
e33ab7366d
Merge pull request #12 from maddalax/attr-ordered
Update attribute methods to use an ordered map so we can have deterministic output
2024-09-30 13:04:19 -05:00
maddalax
e5c5014812 more tests 2024-09-30 13:01:51 -05:00
maddalax
3c4583c2b3 cleanup 2024-09-30 12:47:10 -05:00
maddalax
7b83e2fde7 update attrs to use ordered map 2024-09-30 12:39:48 -05:00
maddalax
fe0e6a5328
Update README.md 2024-09-30 09:24:06 -05:00
github-actions[bot]
82a3d793d1 Auto-update HTMGO framework version 2024-09-30 14:18:41 +00:00
maddalax
0fa096ea2f Merge remote-tracking branch 'origin/master' 2024-09-30 09:17:56 -05:00
maddalax
e542d3dea4 add tests for js commands 2024-09-30 09:17:52 -05:00
github-actions[bot]
d00eed7894 Auto-update HTMGO dependencies 2024-09-29 19:15:36 +00:00
maddalax
887da31598 Merge remote-tracking branch 'origin/master' 2024-09-29 14:14:54 -05:00
maddalax
963e198a74 fix x scroll 2024-09-29 14:14:49 -05:00
github-actions[bot]
b9a0911e29 Auto-update HTMGO dependencies 2024-09-29 17:41:21 +00:00
maddalax
c727b98b17 Merge remote-tracking branch 'origin/master' 2024-09-29 12:40:43 -05:00
maddalax
3fbbeec573 fix example page on mobile 2024-09-29 12:40:39 -05:00
github-actions[bot]
7e64da504c Auto-update HTMGO dependencies 2024-09-29 16:51:15 +00:00
maddalax
46ce943e10 Merge remote-tracking branch 'origin/master' 2024-09-29 11:50:35 -05:00
maddalax
b8f38f64d8 add more js methods 2024-09-29 11:50:30 -05:00
maddalax
50b86007cf add EvalJs on Sibling, EvalJs on Children, EvalJs on Parent 2024-09-29 11:45:52 -05:00
github-actions[bot]
588d715e7a Auto-update HTMGO dependencies 2024-09-29 14:45:59 +00:00
maddalax
3337869c0d Merge remote-tracking branch 'origin/master' 2024-09-29 09:45:21 -05:00
maddalax
fd0d72362f some fixes for childlist 2024-09-29 09:45:17 -05:00
github-actions[bot]
72f4b72755 Auto-update HTMGO dependencies 2024-09-29 14:19:03 +00:00
maddalax
ec30a4e7e8 Merge remote-tracking branch 'origin/master' 2024-09-29 09:18:27 -05:00
maddalax
ca22942ab1 fix typos 2024-09-29 09:18:22 -05:00
github-actions[bot]
da10a54d7e Auto-update HTMGO dependencies 2024-09-29 14:07:46 +00:00
maddalax
057ca2cec4
Update README.md 2024-09-29 09:07:13 -05:00
github-actions[bot]
8f9de65318 Auto-update HTMGO dependencies 2024-09-29 13:48:33 +00:00
maddalax
0cb22da810 Merge remote-tracking branch 'origin/master' 2024-09-29 08:47:43 -05:00
maddalax
10d5d7b220 add submit form on enter 2024-09-29 08:47:39 -05:00
github-actions[bot]
893ae6ee91 Auto-update HTMGO dependencies 2024-09-29 13:28:56 +00:00
maddalax
73adc3339d Merge remote-tracking branch 'origin/master' 2024-09-29 08:28:18 -05:00
maddalax
4ac09a925d disable the form when submitting 2024-09-29 08:28:15 -05:00
github-actions[bot]
7bf4ac9984 Auto-update HTMGO dependencies 2024-09-29 13:23:27 +00:00
maddalax
e84104271b Merge remote-tracking branch 'origin/master' 2024-09-29 08:22:49 -05:00
maddalax
89194e0c20 add github link 2024-09-29 08:22:45 -05:00
github-actions[bot]
95f3931dd0 Auto-update HTMGO dependencies 2024-09-29 13:22:41 +00:00
maddalax
f39b6a5e9e Merge remote-tracking branch 'origin/master' 2024-09-29 08:22:03 -05:00
maddalax
b381db1714 add form example 2024-09-29 08:21:58 -05:00
github-actions[bot]
3b589c9fbf Auto-update HTMGO dependencies 2024-09-29 06:37:51 +00:00
maddalax
3b55ec4921 tidy 2024-09-29 01:37:11 -05:00
maddalax
041d88e670 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	examples/todo-list/go.sum
2024-09-29 01:36:50 -05:00
maddalax
13538191c9 update tasks so they are by ip due to spammers 2024-09-29 01:36:39 -05:00
github-actions[bot]
df7696a512 Auto-update HTMGO dependencies 2024-09-29 06:06:05 +00:00
maddalax
fce0dfe746 Merge remote-tracking branch 'origin/master' 2024-09-29 01:05:28 -05:00
maddalax
66fa5c2cf0 add some more attributes 2024-09-29 01:05:22 -05:00
github-actions[bot]
7563f235cb Auto-update HTMGO dependencies 2024-09-29 05:46:37 +00:00
maddalax
c045e880a7 Merge remote-tracking branch 'origin/master' 2024-09-29 00:45:59 -05:00
maddalax
b972b17515 add some validation 2024-09-29 00:45:54 -05:00
github-actions[bot]
990191d13e Auto-update HTMGO dependencies 2024-09-29 02:03:46 +00:00
maddalax
7a63210bdf Merge remote-tracking branch 'origin/master' 2024-09-28 21:03:01 -05:00
maddalax
bddb528a2d fixes for html symbols 2024-09-28 21:02:57 -05:00
github-actions[bot]
3863dd10f6 Auto-update HTMGO dependencies 2024-09-29 01:53:58 +00:00
maddalax
e0aaf9e603 Merge remote-tracking branch 'origin/master' 2024-09-28 20:53:12 -05:00
maddalax
c239f4eb10 small fix 2024-09-28 20:53:08 -05:00
github-actions[bot]
91f6395509 Auto-update HTMGO dependencies 2024-09-29 01:52:58 +00:00
maddalax
9a59eb35a8 Merge remote-tracking branch 'origin/master' 2024-09-28 20:52:15 -05:00
maddalax
073151cf14 add docs about raw html 2024-09-28 20:52:11 -05:00
github-actions[bot]
b0596af18d Auto-update HTMGO dependencies 2024-09-29 01:46:17 +00:00
maddalax
3d62d4edde Merge remote-tracking branch 'origin/master' 2024-09-28 20:45:33 -05:00
maddalax
2d333a6e0a escape Text input, rename Raw to UnsafeRaw 2024-09-28 20:45:27 -05:00
github-actions[bot]
f972ef27c7 Auto-update HTMGO dependencies 2024-09-29 01:29:22 +00:00
maddalax
4d95e21316 Merge remote-tracking branch 'origin/master' 2024-09-28 20:28:40 -05:00
maddalax
9566b32558 santize input 2024-09-28 20:28:35 -05:00
github-actions[bot]
b1af1bd876 Auto-update HTMGO dependencies 2024-09-28 17:52:13 +00:00
maddalax
7b9be43d4a Merge remote-tracking branch 'origin/master' 2024-09-28 12:51:27 -05:00
maddalax
07b17b3c82 set log level 2024-09-28 12:51:23 -05:00
github-actions[bot]
f19629afde Auto-update HTMGO dependencies 2024-09-28 17:41:29 +00:00
maddalax
47ceddb1f1 Merge remote-tracking branch 'origin/master' 2024-09-28 12:40:52 -05:00
maddalax
513c0fb432 add more sleep time for the cache cleaner 2024-09-28 12:40:48 -05:00
github-actions[bot]
2178d512c6 Auto-update HTMGO dependencies 2024-09-28 17:39:23 +00:00
maddalax
a29f658661 Merge remote-tracking branch 'origin/master' 2024-09-28 12:38:46 -05:00
maddalax
af81f53160 fix expiration for non by key cache
add goroutine to clean up old cache entries to save mem
2024-09-28 12:38:42 -05:00
github-actions[bot]
dc7ae72bd4 Auto-update HTMGO dependencies 2024-09-28 16:57:11 +00:00
maddalax
beeb8bbe30 Merge remote-tracking branch 'origin/master' 2024-09-28 11:56:33 -05:00
maddalax
61e5554f20 make sure the expiration is per key too 2024-09-28 11:56:29 -05:00
github-actions[bot]
6afd742aa8 Auto-update HTMGO dependencies 2024-09-28 16:27:52 +00:00
maddalax
954bd1f3ca Merge remote-tracking branch 'origin/master' 2024-09-28 11:27:11 -05:00
maddalax
17bb55655e add caching per key 2024-09-28 11:27:07 -05:00
github-actions[bot]
ca62712878 Auto-update HTMGO dependencies 2024-09-28 03:46:35 +00:00
maddalax
8c07bd7219 Merge remote-tracking branch 'origin/master' 2024-09-27 22:45:46 -05:00
maddalax
b70796ad28 make the sidebar fixed 2024-09-27 22:45:42 -05:00
github-actions[bot]
568a3d4f52 Auto-update HTMGO dependencies 2024-09-28 02:45:40 +00:00
maddalax
fa61cdc6e5 Merge remote-tracking branch 'origin/master' 2024-09-27 21:44:54 -05:00
maddalax
988464db85 add 1 more important note about arguments not mattering 2024-09-27 21:44:50 -05:00
github-actions[bot]
1a67b485ae Auto-update HTMGO dependencies 2024-09-28 02:42:17 +00:00
maddalax
09141314d8 add github stars example 2024-09-27 21:41:32 -05:00
github-actions[bot]
97a48ea9c6 Auto-update HTMGO dependencies 2024-09-28 02:31:00 +00:00
maddalax
cd93bc956a Merge remote-tracking branch 'origin/master' 2024-09-27 21:29:58 -05:00
maddalax
010ab1fdd6 add caching component support 2024-09-27 21:29:53 -05:00
github-actions[bot]
09a22ce21a Auto-update HTMGO dependencies 2024-09-27 21:25:52 +00:00
maddalax
3ee6a35b73 Merge remote-tracking branch 'origin/master' 2024-09-27 16:25:04 -05:00
maddalax
e35b19e16f remove old files 2024-09-27 16:25:00 -05:00
github-actions[bot]
e1e54fe248 Auto-update HTMGO dependencies 2024-09-27 21:19:04 +00:00
maddalax
2f5842133e Merge remote-tracking branch 'origin/master' 2024-09-27 16:18:15 -05:00
maddalax
1ac2d630c7 dont close void tags 2024-09-27 16:18:09 -05:00
maddalax
0f04094533 add a few missing tags 2024-09-27 15:57:11 -05:00
github-actions[bot]
ab1854f8d2 Auto-update HTMGO dependencies 2024-09-27 15:29:07 +00:00
maddalax
61a9d8e80f updates 2024-09-27 10:28:24 -05:00
github-actions[bot]
96f3baf3c4 Auto-update HTMGO dependencies 2024-09-27 15:23:40 +00:00
maddalax
2456beb494 Merge remote-tracking branch 'origin/master' 2024-09-27 10:22:57 -05:00
maddalax
26fa1a0ed4 release site more often 2024-09-27 10:22:53 -05:00
github-actions[bot]
b10912cb75 Auto-update HTMGO dependencies 2024-09-27 15:21:39 +00:00
maddalax
1208857c5f static / dynamic test 2024-09-27 10:20:50 -05:00
github-actions[bot]
688d2e5090 Auto-update HTMGO dependencies 2024-09-27 15:10:46 +00:00
maddalax
681b873eb0 Merge remote-tracking branch 'origin/master' 2024-09-27 10:10:04 -05:00
maddalax
8b34b63990 some optimizations 2024-09-27 10:10:00 -05:00
github-actions[bot]
d9465cfdd6 Auto-update HTMGO dependencies 2024-09-27 14:47:35 +00:00
maddalax
f01ec9da2e Merge remote-tracking branch 'origin/master' 2024-09-27 09:46:50 -05:00
maddalax
cf5171c237 add benchmarks 2024-09-27 09:46:45 -05:00
github-actions[bot]
1b5e4ebc33 Auto-update HTMGO dependencies 2024-09-27 02:19:40 +00:00
maddalax
3c4927f918 Merge remote-tracking branch 'origin/master' 2024-09-26 21:19:01 -05:00
maddalax
50af00520e dont fill the star 2024-09-26 21:18:58 -05:00
github-actions[bot]
07ccf029a9 Auto-update HTMGO dependencies 2024-09-27 02:15:54 +00:00
maddalax
a8b79ea8e3 Merge remote-tracking branch 'origin/master' 2024-09-26 21:15:09 -05:00
maddalax
ce111ec03d fix github button from being glitchy 2024-09-26 21:15:04 -05:00
github-actions[bot]
796a3ed4ba Auto-update HTMGO dependencies 2024-09-26 22:01:49 +00:00
maddalax
f2c4574e96 Merge remote-tracking branch 'origin/master' 2024-09-26 17:00:34 -05:00
maddalax
0034625198 mobile nav fixes 2024-09-26 17:00:30 -05:00
github-actions[bot]
4ff8503acc Auto-update HTMGO dependencies 2024-09-26 21:13:01 +00:00
maddalax
80b3a1d1a6 Merge remote-tracking branch 'origin/master' 2024-09-26 16:12:24 -05:00
maddalax
3339a85123 framework license 2024-09-26 16:12:21 -05:00
github-actions[bot]
5d3a878cf6 Auto-update HTMGO dependencies 2024-09-26 21:02:33 +00:00
maddalax
dc4b710ef2 Merge remote-tracking branch 'origin/master' 2024-09-26 16:01:45 -05:00
maddalax
e77ab0bc28 fix framework tests (sort attributes) 2024-09-26 16:01:41 -05:00
github-actions[bot]
ce9c7297c7 Auto-update HTMGO dependencies 2024-09-26 20:47:21 +00:00
maddalax
9d0f397c04 dont need to check 2024-09-26 15:46:41 -05:00
maddalax
63b9bd3c5b add back action to keep modules updated with framework 2024-09-26 15:44:17 -05:00
maddalax
60b1646687 add proxy to install instructions 2024-09-26 15:41:57 -05:00
maddalax
b108b8aead dont update here 2024-09-26 15:40:17 -05:00
maddalax
60476e59d6 dont download a version 2024-09-26 15:38:34 -05:00
maddalax
ea8c3c6891 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	examples/todo-list/go.mod
#	examples/todo-list/go.sum
#	htmgo-site/go.mod
#	htmgo-site/go.sum
#	templates/starter/go.mod
#	templates/starter/go.sum
2024-09-26 15:35:58 -05:00
maddalax
6bd864f29a caching will be the end of me 2024-09-26 15:35:50 -05:00
maddalax
85719226d8 caching will be the end of me 2024-09-26 15:35:04 -05:00
github-actions[bot]
7549883f14 Auto-update HTMGO dependencies 2024-09-26 20:29:20 +00:00
maddalax
ac4c35aae4 fix go.sum 2024-09-26 15:28:39 -05:00
maddalax
1930e95c15 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	templates/starter/go.sum
2024-09-26 15:27:30 -05:00
maddalax
9b74af1734 fix 2024-09-26 15:27:20 -05:00
github-actions[bot]
ab8d37f040 Auto-update HTMGO dependencies 2024-09-26 20:26:03 +00:00
maddalax
cb57375f63 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	framework-ui/go.sum
#	templates/starter/go.sum
2024-09-26 15:25:06 -05:00
maddalax
db08f98ae0 fix 2024-09-26 15:24:44 -05:00
github-actions[bot]
2d26bd2c4e Auto-update HTMGO dependencies 2024-09-26 20:11:50 +00:00
maddalax
44fe5ab255 will this work 2024-09-26 15:09:55 -05:00
maddalax
aa6227ea1a test 2024-09-26 15:08:07 -05:00
maddalax
5d72639fba update framework deps on commit 2024-09-26 15:06:04 -05:00
maddalax
33b5067e76 upgrade deps 2024-09-26 15:03:25 -05:00
maddalax
4d003b6db2
Merge pull request #8 from maddalax/remove-echo
wip - swap out echo with std lib + chi router
2024-09-26 12:55:23 -07:00
411 changed files with 20184 additions and 1822 deletions

3
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [maddalax]

View file

@ -0,0 +1,52 @@
name: Build and Deploy htmgo auth example
on:
workflow_run:
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
types:
- completed
workflow_dispatch: # Trigger on manual workflow_dispatch
push:
branches:
- master # Trigger on pushes to master
paths:
- 'examples/simple-auth/**' # Trigger only if files in this directory change
- "framework-ui/**"
- "cli/**"
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get short commit hash
id: vars
run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
- name: Build Docker image
run: |
cd ./examples/simple-auth && docker build -t ghcr.io/${{ github.repository_owner }}/simple-auth:${{ steps.vars.outputs.short_sha }} .
- name: Tag as latest Docker image
run: |
docker tag ghcr.io/${{ github.repository_owner }}/simple-auth:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/simple-auth:latest
- name: Log in to GitHub Container Registry
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push Docker image
run: |
docker push ghcr.io/${{ github.repository_owner }}/simple-auth:latest

View file

@ -0,0 +1,50 @@
name: Build and Deploy htmgo.dev chat example
on:
workflow_run:
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
types:
- completed
workflow_dispatch: # Trigger on manual workflow_dispatch
push:
branches:
- master # Trigger on pushes to master
paths:
- 'examples/chat/**' # Trigger only if files in this directory change
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get short commit hash
id: vars
run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
- name: Build Docker image
run: |
cd ./examples/chat && docker build -t ghcr.io/${{ github.repository_owner }}/htmgo-chat-example:${{ steps.vars.outputs.short_sha }} .
- name: Tag as latest Docker image
run: |
docker tag ghcr.io/${{ github.repository_owner }}/htmgo-chat-example:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/htmgo-chat-example:latest
- name: Log in to GitHub Container Registry
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push Docker image
run: |
docker push ghcr.io/${{ github.repository_owner }}/htmgo-chat-example:latest

52
.github/workflows/release-hn-clone.yml vendored Normal file
View file

@ -0,0 +1,52 @@
name: Build and Deploy htmgo hackernews clone
on:
workflow_run:
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
types:
- completed
workflow_dispatch: # Trigger on manual workflow_dispatch
push:
branches:
- master # Trigger on pushes to master
paths:
- 'examples/hackernews/**' # Trigger only if files in this directory change
- "framework-ui/**"
- "cli/**"
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get short commit hash
id: vars
run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
- name: Build Docker image
run: |
cd ./examples/hackernews && docker build -t ghcr.io/${{ github.repository_owner }}/hackernews:${{ steps.vars.outputs.short_sha }} .
- name: Tag as latest Docker image
run: |
docker tag ghcr.io/${{ github.repository_owner }}/hackernews:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/hackernews:latest
- name: Log in to GitHub Container Registry
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push Docker image
run: |
docker push ghcr.io/${{ github.repository_owner }}/hackernews:latest

View file

@ -1,12 +1,18 @@
name: Build and Deploy htmgo.dev
on:
workflow_run:
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
types:
- completed
workflow_dispatch: # Trigger on manual workflow_dispatch
push:
branches:
- master # Trigger on pushes to master
paths:
- 'htmgo-site/**' # Trigger only if files in this directory change
- "framework-ui/**"
- "cli/**"
jobs:
build-and-push:

View file

@ -1,6 +1,10 @@
name: Build and Deploy starter template
on:
workflow_run:
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
types:
- completed
workflow_dispatch: # Trigger on manual workflow_dispatch
push:
branches:

View file

@ -1,6 +1,10 @@
name: Build and Deploy htmgo.dev todo example
on:
workflow_run:
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
types:
- completed
workflow_dispatch: # Trigger on manual workflow_dispatch
push:
branches:

48
.github/workflows/release-ws-test.yml vendored Normal file
View file

@ -0,0 +1,48 @@
name: Build and Deploy ws-test
on:
workflow_run:
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
types:
- completed
workflow_dispatch:
push:
branches:
- ws-testing
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get short commit hash
id: vars
run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
- name: Build Docker image
run: |
cd ./examples/ws-example && docker build -t ghcr.io/${{ github.repository_owner }}/ws-example:${{ steps.vars.outputs.short_sha }} .
- name: Tag as latest Docker image
run: |
docker tag ghcr.io/${{ github.repository_owner }}/ws-example:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/ws-example:latest
- name: Log in to GitHub Container Registry
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push Docker image
run: |
docker push ghcr.io/${{ github.repository_owner }}/ws-example:latest

33
.github/workflows/run-cli-tests.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: CLI Tests
on:
push:
branches:
- master
pull_request:
branches:
- '**' # Runs on any pull request to any branch
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23' # Specify the Go version you need
- name: Install dependencies
run: cd ./cli/htmgo && go mod download
- name: Run Go tests
run: cd ./cli/htmgo/tasks/astgen && go test ./... -coverprofile=coverage.txt
- name: Upload results to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -3,7 +3,7 @@ name: Framework Tests
on:
push:
branches:
- '**' # Runs on any branch push
- master
pull_request:
branches:
- '**' # Runs on any pull request to any branch
@ -25,4 +25,9 @@ jobs:
run: cd ./framework && go mod download
- name: Run Go tests
run: cd ./framework && go test ./...
run: cd ./framework && go test ./... -coverprofile=coverage.txt
- name: Upload results to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -0,0 +1,39 @@
name: Update HTMGO Framework Dependency
on:
workflow_dispatch: # Trigger on manual workflow_dispatch
push:
branches:
- master # Trigger on pushes to master
paths:
- 'framework/**'
- 'tools/html-to-htmgo/**'
jobs:
update-htmgo-dep:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
persist-credentials: false # Necessary to avoid using the runner's credentials for commit
fetch-depth: 0 # Full history for committing back changes
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ">=1.20"
- name: Run update-htmgo-dep.go script
run: go run tools/update-htmgo-dep.go
- name: Commit changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
git commit -m "Auto-update HTMGO framework version"
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

128
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
maddox@htmgo.dev.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View file

@ -1,14 +1,20 @@
> [!WARNING]
> htmgo is in alpha release. Please report any issues on GitHub.
## **htmgo**
### build simple and scalable systems with go + htmx
-------
[![Go Report Card](https://goreportcard.com/badge/github.com/maddalax/htmgo)](https://goreportcard.com/report/github.com/maddalax/htmgo)
![Build](https://github.com/maddalax/htmgo/actions/workflows/run-framework-tests.yml/badge.svg)
[![Go Reference](https://pkg.go.dev/badge/github.com/maddalax/htmgo/framework@v1.0.2/h.svg)](https://htmgo.dev/docs)
[![codecov](https://codecov.io/github/maddalax/htmgo/graph/badge.svg?token=ANPD11LSGN)](https://codecov.io/github/maddalax/htmgo)
[![Join Discord](https://img.shields.io/badge/Join%20Discord-gray?style=flat&logo=discord&logoColor=white&link=https://htmgo.dev/discord)](https://htmgo.dev/discord)
![GitHub Sponsors](https://img.shields.io/github/sponsors/maddalax)
<sup>looking for a python version? check out: https://fastht.ml</sup>
**introduction:**
htmgo is a lightweight pure go way to build interactive websites / web applications using go & htmx.
@ -33,9 +39,12 @@ func IndexPage(ctx *h.RequestContext) *h.Page {
2. live reload (rebuilds css, go, ent schema, and routes upon change)
3. automatic page and partial registration based on file path
4. built in tailwindcss support, no need to configure anything by default
5. plugin architecture to include optional plugins to streamline development, such as http://entgo.io
6. custom [htmx extensions](https://github.com/maddalax/htmgo/tree/b610aefa36e648b98a13823a6f8d87566120cfcc/framework/assets/js/htmxextensions) to reduce boilerplate with common tasks
5. custom [htmx extensions](https://github.com/maddalax/htmgo/tree/b610aefa36e648b98a13823a6f8d87566120cfcc/framework/assets/js/htmxextensions) to reduce boilerplate with common tasks
**get started:**
View documentation on [htmgo.dev](https://htmgo.dev/docs).
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=maddalax/htmgo&type=Date)](https://star-history.com/#maddalax/htmgo&Date)

View file

@ -3,11 +3,25 @@ module github.com/maddalax/htmgo/cli/htmgo
go 1.23.0
require (
github.com/dave/jennifer v1.7.1
github.com/fsnotify/fsnotify v1.7.0
github.com/google/uuid v1.6.0
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
github.com/maddalax/htmgo/tools/html-to-htmgo v0.0.0-20250703190716-06f01b3d7c1b
github.com/stretchr/testify v1.9.0
golang.org/x/mod v0.21.0
golang.org/x/net v0.29.0
golang.org/x/sys v0.25.0
golang.org/x/sys v0.26.0
golang.org/x/tools v0.25.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
)
require (
github.com/bmatcuk/doublestar/v4 v4.7.1
github.com/go-chi/chi/v5 v5.1.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/text v0.19.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

32
cli/htmgo/go.sum Normal file
View file

@ -0,0 +1,32 @@
github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
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/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
github.com/maddalax/htmgo/tools/html-to-htmgo v0.0.0-20250703190716-06f01b3d7c1b h1:jvfp35fig2TzBjAgw82fe8+7cvaLX9EbipZUlj8FDDY=
github.com/maddalax/htmgo/tools/html-to-htmgo v0.0.0-20250703190716-06f01b3d7c1b/go.mod h1:FraJsj3NRuLBQDk83ZVa+psbNRNLe+rajVtVhYMEme4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,119 +0,0 @@
package main
import (
"bytes"
"fmt"
"log"
"strings"
"github.com/dave/jennifer/jen"
"golang.org/x/net/html"
)
func main() {
// Example HTML input
htmlData := `
<body><nav class="flex gap-4 items-center p-4 text-slate-600 "><a href="/" class="cursor-pointer hover:text-blue-400 ">Home</a><a class="cursor-pointer hover:text-blue-400 " href="/news">News</a><a href="/patients" class="cursor-pointer hover:text-blue-400 ">Patients</a></nav><div id="active-modal"></div><div class="flex flex-col gap-2 bg-white h-full "><div class="flex flex-col p-4 w-full "><div><div class="flex justify-between items-center "><p class="text-lg font-bold ">Manage Patients</p><button hx-target="#active-modal" type="button" id="add-patient" class="flex gap-1 items-center border p-4 rounded cursor-hover bg-blue-700 text-white rounded p-2 h-12 " hx-get="htmgo/partials/patient.AddPatientSheet">Add Patient</button></div><div hx-get="htmgo/partials/patient.List" hx-trigger="load, patient-added from:body" class=""><div class="mt-8" id="patient-list"><div class="flex flex-col gap-2 rounded p-4 bg-red-100 "><p>Name: Sydne</p><p>Reason for visit: arm hurts</p></div></div></div></div></div></div><div hx-get="/livereload" hx-trigger="every 200ms" class=""></div></body>
`
// Parse the HTML
doc, err := html.Parse(bytes.NewReader([]byte(htmlData)))
if err != nil {
log.Fatal(err)
}
// Create a new Jennifer file
f := jen.NewFile("main")
// Generate Jennifer code for the parsed HTML tree
generatedCode := processNode(doc.FirstChild)
// Add the generated code to the file
f.Func().Id("Render").Params().Block(generatedCode...)
// Render the generated code
var buf bytes.Buffer
err = f.Render(&buf)
if err != nil {
log.Fatal(err)
}
//// Format the generated code
//formattedCode, err := format.Source(buf.Bytes())
//if err != nil {
// log.Fatal(err)
//}
// Output the formatted code
fmt.Println(string(buf.Bytes()))
}
// Recursively process the HTML nodes and generate Jennifer code
func processNode(n *html.Node) []jen.Code {
var code []jen.Code
// Only process element nodes
if n.Type == html.ElementNode {
// Create a dynamic method call based on the tag name
tagMethod := strings.Title(n.Data) // Capitalize the first letter of the tag
// Add dynamic method call for the tag (e.g., h.Div(), h.Button(), etc.)
code = append(code, jen.Id("h").Dot(tagMethod).Call(mergeArgs(n)...))
}
return code
}
// Merge attributes and children into a single slice for Call()
func mergeArgs(n *html.Node) []jen.Code {
// Process attributes
attrs := processAttributes(n.Attr)
// Process children
children := processChildren(n)
// Combine attributes and children into one slice
return append(attrs, children...)
}
// Process child nodes of a given HTML node
func processChildren(n *html.Node) []jen.Code {
var children []jen.Code
for c := n.FirstChild; c != nil; c = c.NextSibling {
children = append(children, processNode(c)...)
}
return children
}
func FormatFieldName(name string) string {
split := strings.Split(name, "_")
if strings.Contains(name, "-") {
split = strings.Split(name, "-")
}
parts := make([]string, 0)
for _, s := range split {
parts = append(parts, PascalCase(s))
}
return strings.Join(parts, "")
}
func PascalCase(s string) string {
if s == "" {
return s
}
// Convert the first rune (character) to uppercase and concatenate with the rest of the string
return strings.ToUpper(string(s[0])) + s[1:]
}
// Process the attributes of an HTML node and return Jennifer code
func processAttributes(attrs []html.Attribute) []jen.Code {
var args []jen.Code
for _, attr := range attrs {
// Dynamically handle all attributes
attrMethod := FormatFieldName(attr.Key) // E.g., convert "data-role" to "DataRole"
args = append(args, jen.Id("h").Dot(attrMethod).Call(jen.Lit(attr.Val)))
}
return args
}

View file

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
"github.com/maddalax/htmgo/framework/config"
"io"
"log/slog"
"os"
@ -17,6 +18,16 @@ func HasFileFromRoot(file string) bool {
return err == nil
}
func GetConfig() *config.ProjectConfig {
return config.FromConfigFile(process.GetWorkingDir())
}
func CreateHtmgoDir() {
if !HasFileFromRoot("__htmgo") {
CreateDirFromRoot("__htmgo")
}
}
func CreateDirFromRoot(dir string) error {
cwd := process.GetWorkingDir()
path := filepath.Join(cwd, dir)

View file

@ -0,0 +1,33 @@
package dirutil
import (
"fmt"
"github.com/bmatcuk/doublestar/v4"
"strings"
)
func matchesAny(patterns []string, path string) bool {
for _, pattern := range patterns {
matched, err :=
doublestar.Match(strings.ReplaceAll(pattern, `\`, "/"), strings.ReplaceAll(path, `\`, "/"))
if err != nil {
fmt.Printf("Error matching pattern: %v\n", err)
return false
}
if matched {
return true
}
}
return false
}
func IsGlobExclude(path string, excludePatterns []string) bool {
return matchesAny(excludePatterns, path)
}
func IsGlobMatch(path string, patterns []string, excludePatterns []string) bool {
if matchesAny(excludePatterns, path) {
return false
}
return matchesAny(patterns, path)
}

View file

@ -5,25 +5,26 @@ import (
"flag"
"fmt"
"github.com/maddalax/htmgo/cli/htmgo/internal"
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
"github.com/maddalax/htmgo/cli/htmgo/tasks/astgen"
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
"github.com/maddalax/htmgo/cli/htmgo/tasks/css"
"github.com/maddalax/htmgo/cli/htmgo/tasks/downloadtemplate"
"github.com/maddalax/htmgo/cli/htmgo/tasks/formatter"
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
"github.com/maddalax/htmgo/cli/htmgo/tasks/reloader"
"github.com/maddalax/htmgo/cli/htmgo/tasks/run"
"log/slog"
"os"
"strings"
"sync"
)
const version = "1.0.6"
func main() {
done := RegisterSignals()
needsSignals := true
commandMap := make(map[string]*flag.FlagSet)
commands := []string{"template", "run", "watch", "build", "setup", "css", "schema", "generate"}
commands := []string{"template", "run", "watch", "build", "setup", "css", "schema", "generate", "format", "version"}
for _, command := range commands {
commandMap[command] = flag.NewFlagSet(command, flag.ExitOnError)
@ -57,8 +58,13 @@ func main() {
slog.Debug("Running task:", slog.String("task", taskName))
slog.Debug("working dir:", slog.String("dir", process.GetWorkingDir()))
if !dirutil.HasFileFromRoot("__htmgo") {
dirutil.CreateDirFromRoot("__htmgo")
if taskName == "format" {
needsSignals = false
}
done := make(chan bool, 1)
if needsSignals {
done = RegisterSignals()
}
if taskName == "watch" {
@ -72,21 +78,9 @@ func main() {
fmt.Printf("Generating CSS...\n")
css.GenerateCss(process.ExitOnError)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
// generate ast needs to be run after css generation
astgen.GenAst(process.ExitOnError)
}()
wg.Add(1)
go func() {
defer wg.Done()
run.EntGenerate()
}()
wg.Wait()
fmt.Printf("Starting server...\n")
process.KillAll()
@ -95,7 +89,22 @@ func main() {
}()
startWatcher(reloader.OnFileChange)
} else {
if taskName == "schema" {
if taskName == "version" {
fmt.Printf("htmgo cli version %s\n", version)
os.Exit(0)
}
if taskName == "format" {
if len(os.Args) < 3 {
fmt.Println(fmt.Sprintf("Usage: htmgo format <file>"))
os.Exit(1)
}
file := os.Args[2]
if file == "." {
formatter.FormatDir(process.GetWorkingDir())
} else {
formatter.FormatFile(os.Args[2])
}
} else if taskName == "schema" {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter entity name:")
text, _ := reader.ReadString('\n')
@ -109,10 +118,10 @@ func main() {
} else if taskName == "css" {
_ = css.GenerateCss(process.ExitOnError)
} else if taskName == "ast" {
css.GenerateCss(process.ExitOnError)
_ = astgen.GenAst(process.ExitOnError)
} else if taskName == "run" {
_ = astgen.GenAst(process.ExitOnError)
_ = css.GenerateCss(process.ExitOnError)
run.MakeBuildable()
_ = run.Server(process.ExitOnError)
} else if taskName == "template" {
name := ""

View file

@ -2,15 +2,21 @@ package astgen
import (
"fmt"
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
"go/ast"
"go/parser"
"go/token"
"golang.org/x/mod/modfile"
"io/fs"
"log/slog"
"os"
"path/filepath"
"slices"
"strings"
"unicode"
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
"github.com/maddalax/htmgo/framework/h"
"golang.org/x/mod/modfile"
)
type Page struct {
@ -24,6 +30,7 @@ type Partial struct {
FuncName string
Package string
Import string
Path string
}
const GeneratedDirName = "__htmgo"
@ -34,6 +41,36 @@ const ModuleName = "github.com/maddalax/htmgo/framework/h"
var PackageName = fmt.Sprintf("package %s", GeneratedDirName)
var GeneratedFileLine = fmt.Sprintf("// Package %s THIS FILE IS GENERATED. DO NOT EDIT.", GeneratedDirName)
func toPascaleCase(input string) string {
words := strings.Split(input, "_")
for i := range words {
words[i] = strings.Title(strings.ToLower(words[i]))
}
return strings.Join(words, "")
}
func isValidGoVariableName(name string) bool {
// Variable name must not be empty
if name == "" {
return false
}
// First character must be a letter or underscore
if !unicode.IsLetter(rune(name[0])) && name[0] != '_' {
return false
}
// Remaining characters must be letters, digits, or underscores
for _, char := range name[1:] {
if !unicode.IsLetter(char) && !unicode.IsDigit(char) && char != '_' {
return false
}
}
return true
}
func normalizePath(path string) string {
return strings.ReplaceAll(path, `\`, "/")
}
func sliceCommonPrefix(dir1, dir2 string) string {
// Use filepath.Clean to normalize the paths
dir1 = filepath.Clean(dir1)
@ -59,9 +96,35 @@ func sliceCommonPrefix(dir1, dir2 string) string {
// Return the longer one
if len(slicedDir1) > len(slicedDir2) {
return slicedDir1
return normalizePath(slicedDir1)
}
return slicedDir2
return normalizePath(slicedDir2)
}
func hasOnlyReqContextParam(funcType *ast.FuncType) bool {
if len(funcType.Params.List) != 1 {
return false
}
if funcType.Params.List[0].Names == nil {
return false
}
if len(funcType.Params.List[0].Names) != 1 {
return false
}
t := funcType.Params.List[0].Type
name, ok := t.(*ast.StarExpr)
if !ok {
return false
}
selectorExpr, ok := name.X.(*ast.SelectorExpr)
if !ok {
return false
}
ident, ok := selectorExpr.X.(*ast.Ident)
if !ok {
return false
}
return ident.Name == "h" && selectorExpr.Sel.Name == "RequestContext"
}
func findPublicFuncsReturningHPartial(dir string, predicate func(partial Partial) bool) ([]Partial, error) {
@ -100,10 +163,11 @@ func findPublicFuncsReturningHPartial(dir string, predicate func(partial Partial
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" {
if selectorExpr.Sel.Name == "Partial" && hasOnlyReqContextParam(funcDecl.Type) {
p := Partial{
Package: node.Name.Name,
Import: sliceCommonPrefix(cwd, strings.ReplaceAll(filepath.Dir(path), `\`, `/`)),
Path: normalizePath(sliceCommonPrefix(cwd, path)),
Import: sliceCommonPrefix(cwd, normalizePath(filepath.Dir(path))),
FuncName: funcDecl.Name.Name,
}
if predicate(p) {
@ -166,11 +230,11 @@ func findPublicFuncsReturningHPage(dir string) ([]Page, error) {
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" {
if selectorExpr.Sel.Name == "Page" && hasOnlyReqContextParam(funcDecl.Type) {
pages = append(pages, Page{
Package: node.Name.Name,
Import: strings.ReplaceAll(filepath.Dir(path), `\`, `/`),
Path: path,
Import: normalizePath(filepath.Dir(path)),
Path: normalizePath(path),
FuncName: funcDecl.Name.Name,
})
break
@ -196,70 +260,51 @@ func findPublicFuncsReturningHPage(dir string) ([]Page, error) {
}
func buildGetPartialFromContext(builder *CodeBuilder, partials []Partial) {
fName := "GetPartialFromContext"
body := `
path := r.URL.Path
`
if len(partials) == 0 {
body = ""
}
moduleName := GetModuleName()
for _, f := range partials {
if f.FuncName == fName {
continue
}
caller := fmt.Sprintf("%s.%s", f.Package, f.FuncName)
path := fmt.Sprintf("/%s/%s.%s", moduleName, f.Import, f.FuncName)
body += fmt.Sprintf(`
if path == "%s" || path == "%s" {
var routerHandlerMethod = func(path string, caller string) string {
return fmt.Sprintf(`
router.Handle("%s", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext)
return %s(cc)
}
`, f.FuncName, path, caller)
}
body += "return nil"
f := Function{
Name: fName,
Parameters: []NameType{
{Name: "r", Type: "*http.Request"},
},
Return: []ReturnType{
{Type: "*h.Partial"},
},
Body: body,
}
builder.Append(builder.BuildFunction(f))
registerFunction := fmt.Sprintf(`
func RegisterPartials(router *chi.Mux) {
router.Handle("/%s/partials*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
partial := GetPartialFromContext(r)
partial := %s(cc)
if partial == nil {
w.WriteHeader(404)
return
}
h.PartialView(w, partial)
}))
}))`, path, caller)
}
`, moduleName)
handlerMethods := make([]string, 0)
for _, f := range partials {
caller := fmt.Sprintf("%s.%s", f.Package, f.FuncName)
path := fmt.Sprintf("/%s/%s.%s", moduleName, f.Import, f.FuncName)
handlerMethods = append(handlerMethods, routerHandlerMethod(path, caller))
}
registerFunction := fmt.Sprintf(`
func RegisterPartials(router *chi.Mux) {
%s
}
`, strings.Join(handlerMethods, "\n"))
builder.AppendLine(registerFunction)
}
func writePartialsFile() {
config := dirutil.GetConfig()
cwd := process.GetWorkingDir()
partialPath := filepath.Join(cwd, "partials")
partialPath := filepath.Join(cwd)
partials, err := findPublicFuncsReturningHPartial(partialPath, func(partial Partial) bool {
return partial.FuncName != "GetPartialFromContext"
})
partials = h.Filter(partials, func(partial Partial) bool {
return !dirutil.IsGlobExclude(partial.Path, config.AutomaticPartialRoutingIgnore)
})
if err != nil {
fmt.Println(err)
return
@ -268,9 +313,12 @@ func writePartialsFile() {
builder := NewCodeBuilder(nil)
builder.AppendLine(GeneratedFileLine)
builder.AppendLine(PackageName)
builder.AddImport(ChiModuleName)
if len(partials) > 0 {
builder.AddImport(ModuleName)
builder.AddImport(HttpModuleName)
builder.AddImport(ChiModuleName)
}
moduleName := GetModuleName()
for _, partial := range partials {
@ -293,6 +341,17 @@ func formatRoute(path string) string {
path = strings.ReplaceAll(path, "_", "/")
path = strings.ReplaceAll(path, ".", "/")
path = strings.ReplaceAll(path, "\\", "/")
parts := strings.Split(path, "/")
for i, part := range parts {
if strings.HasPrefix(part, ":") {
parts[i] = fmt.Sprintf("{%s}", part[1:])
}
}
path = strings.Join(parts, "/")
if path == "" {
return "/"
}
@ -306,6 +365,7 @@ func formatRoute(path string) string {
}
func writePagesFile() {
config := dirutil.GetConfig()
builder := NewCodeBuilder(nil)
builder.AppendLine(GeneratedFileLine)
@ -315,6 +375,10 @@ func writePagesFile() {
pages, _ := findPublicFuncsReturningHPage("pages")
pages = h.Filter(pages, func(page Page) bool {
return !dirutil.IsGlobExclude(page.Path, config.AutomaticPageRoutingIgnore)
})
if len(pages) > 0 {
builder.AddImport(ModuleName)
}
@ -360,9 +424,96 @@ func writePagesFile() {
})
}
func writeAssetsFile() {
cwd := process.GetWorkingDir()
config := dirutil.GetConfig()
slog.Debug("writing assets file", slog.String("cwd", cwd), slog.String("config", config.PublicAssetPath))
distAssets := filepath.Join(cwd, "assets", "dist")
hasAssets := false
builder := strings.Builder{}
builder.WriteString(`package assets`)
builder.WriteString("\n")
filepath.WalkDir(distAssets, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if strings.HasPrefix(d.Name(), ".") {
return nil
}
path = strings.ReplaceAll(path, distAssets, "")
httpUrl := normalizePath(fmt.Sprintf("%s%s", config.PublicAssetPath, path))
path = normalizePath(path)
path = strings.ReplaceAll(path, "/", "_")
path = strings.ReplaceAll(path, "//", "_")
name := strings.ReplaceAll(path, ".", "_")
name = strings.ReplaceAll(name, "-", "_")
name = toPascaleCase(name)
if isValidGoVariableName(name) {
builder.WriteString(fmt.Sprintf(`const %s = "%s"`, name, httpUrl))
builder.WriteString("\n")
hasAssets = true
}
return nil
})
builder.WriteString("\n")
str := builder.String()
if hasAssets {
WriteFile(filepath.Join(GeneratedDirName, "assets", "assets-generated.go"), func(content *ast.File) string {
return str
})
}
}
func HasModuleFile(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
func CheckPagesDirectory(path string) error {
pagesPath := filepath.Join(path, "pages")
_, err := os.Stat(pagesPath)
if err != nil {
return fmt.Errorf("The directory pages does not exist.")
}
return nil
}
func GetModuleName() string {
wd := process.GetWorkingDir()
modPath := filepath.Join(wd, "go.mod")
if HasModuleFile(modPath) == false {
fmt.Fprintf(os.Stderr, "Module not found: go.mod file does not exist.")
return ""
}
checkDir := CheckPagesDirectory(wd)
if checkDir != nil {
fmt.Fprintf(os.Stderr, checkDir.Error())
return ""
}
goModBytes, err := os.ReadFile(modPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error reading go.mod: %v\n", err)
@ -381,6 +532,7 @@ func GenAst(flags ...process.RunFlag) error {
}
writePartialsFile()
writePagesFile()
writeAssetsFile()
WriteFile("__htmgo/setup-generated.go", func(content *ast.File) string {

View file

@ -1,82 +0,0 @@
package astgen
// 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
}
}
}
}

View file

@ -0,0 +1,6 @@
/assets/dist
tmp
node_modules
.idea
__htmgo
dist

View file

@ -0,0 +1,13 @@
//go:build !prod
// +build !prod
package main
import (
"astgen-project-sample/internal/embedded"
"io/fs"
)
func GetStaticAssets() fs.FS {
return embedded.NewOsFs()
}

View file

@ -0,0 +1,16 @@
//go:build prod
// +build prod
package main
import (
"embed"
"io/fs"
)
//go:embed assets/dist/*
var staticAssets embed.FS
func GetStaticAssets() fs.FS {
return staticAssets
}

View file

@ -0,0 +1,11 @@
module astgen-project-sample
go 1.23.0
require github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
require (
github.com/go-chi/chi/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -0,0 +1,18 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
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/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,21 @@
# htmgo configuration
# if tailwindcss is enabled, htmgo will automatically compile your tailwind and output it to assets/dist
tailwind: true
# which directories to ignore when watching for changes, supports glob patterns through https://github.com/bmatcuk/doublestar
watch_ignore: [".git", "node_modules", "dist/*"]
# files to watch for changes, supports glob patterns through https://github.com/bmatcuk/doublestar
watch_files: ["**/*.go", "**/*.css", "**/*.md"]
# files or directories to ignore when automatically registering routes for pages
# supports glob patterns through https://github.com/bmatcuk/doublestar
automatic_page_routing_ignore: ["root.go"]
# files or directories to ignore when automatically registering routes for partials
# supports glob patterns through https://github.com/bmatcuk/doublestar
automatic_partial_routing_ignore: []
# url path of where the public assets are located
public_asset_path: "/public"

View file

@ -0,0 +1,17 @@
package embedded
import (
"io/fs"
"os"
)
type OsFs struct {
}
func (receiver OsFs) Open(name string) (fs.File, error) {
return os.Open(name)
}
func NewOsFs() OsFs {
return OsFs{}
}

View file

@ -0,0 +1,36 @@
package main
import (
"astgen-project-sample/__htmgo"
"fmt"
"github.com/maddalax/htmgo/framework/config"
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/service"
"io/fs"
"net/http"
)
func main() {
locator := service.NewLocator()
cfg := config.Get()
h.Start(h.AppOpts{
ServiceLocator: locator,
LiveReload: true,
Register: func(app *h.App) {
sub, err := fs.Sub(GetStaticAssets(), "assets/dist")
if err != nil {
panic(err)
}
http.FileServerFS(sub)
// change this in htmgo.yml (public_asset_path)
app.Router.Handle(fmt.Sprintf("%s/*", cfg.PublicAssetPath),
http.StripPrefix(cfg.PublicAssetPath, http.FileServerFS(sub)))
__htmgo.Register(app.Router)
},
})
}

View file

@ -0,0 +1,30 @@
package pages
import (
"github.com/maddalax/htmgo/framework/h"
)
func IndexPage(ctx *h.RequestContext) *h.Page {
return RootPage(
h.Div(
h.Class("flex flex-col gap-4 items-center pt-24 min-h-screen bg-neutral-100"),
h.H3(
h.Id("intro-text"),
h.Text("hello htmgo"),
h.Class("text-5xl"),
),
h.Div(
h.Class("mt-3"),
),
h.Div(),
),
)
}
func TestPartial(ctx *h.RequestContext) *h.Partial {
return h.NewPartial(
h.Div(
h.Text("Hello World"),
),
)
}

View file

@ -0,0 +1,40 @@
package pages
import (
"github.com/maddalax/htmgo/framework/h"
)
func RootPage(children ...h.Ren) *h.Page {
title := "htmgo template"
description := "an example of the htmgo template"
author := "htmgo"
url := "https://htmgo.dev"
return h.NewPage(
h.Html(
h.HxExtensions(
h.BaseExtensions(),
),
h.Head(
h.Title(
h.Text(title),
),
h.Meta("viewport", "width=device-width, initial-scale=1"),
h.Meta("title", title),
h.Meta("charset", "utf-8"),
h.Meta("author", author),
h.Meta("description", description),
h.Meta("og:title", title),
h.Meta("og:url", url),
h.Link("canonical", url),
h.Meta("og:description", description),
),
h.Body(
h.Div(
h.Class("flex flex-col gap-2 bg-white h-full"),
h.Fragment(children...),
),
),
),
)
}

View file

@ -0,0 +1,18 @@
package partials
import "github.com/maddalax/htmgo/framework/h"
func CountersPartial(ctx *h.RequestContext) *h.Partial {
return h.NewPartial(
h.Div(
h.Text("my counter"),
),
)
}
func SwapFormError(ctx *h.RequestContext, error string) *h.Partial {
return h.SwapPartial(
ctx,
h.Div(),
)
}

View file

@ -0,0 +1,66 @@
package astgen
import (
"fmt"
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
"github.com/stretchr/testify/assert"
"net/http"
"os"
"path/filepath"
"sync"
"testing"
"time"
)
func TestAstGen(t *testing.T) {
t.Parallel()
workingDir, err := filepath.Abs("./project-sample")
assert.NoError(t, err)
process.SetWorkingDir(workingDir)
assert.NoError(t, os.Chdir(workingDir))
err = dirutil.DeleteDir(filepath.Join(process.GetWorkingDir(), "__htmgo"))
assert.NoError(t, err)
err = process.Run(process.NewRawCommand("", "go build ."))
assert.Error(t, err)
err = GenAst()
assert.NoError(t, err)
go func() {
// project was buildable after astgen, confirmed working
err = process.Run(process.NewRawCommand("server", "go run ."))
assert.NoError(t, err)
}()
time.Sleep(time.Second * 1)
urls := []string{
"/astgen-project-sample/partials.CountersPartial",
"/",
"/astgen-project-sample/pages.TestPartial",
}
defer func() {
serverProcess := process.GetProcessByName("server")
assert.NotNil(t, serverProcess)
process.KillProcess(*serverProcess)
}()
wg := sync.WaitGroup{}
for _, url := range urls {
wg.Add(1)
go func() {
defer wg.Done()
// ensure we can get a 200 response on the partials
resp, e := http.Get(fmt.Sprintf("http://localhost:3000%s", url))
assert.NoError(t, e)
assert.Equal(t, http.StatusOK, resp.StatusCode, fmt.Sprintf("%s was not a 200 response", url))
}()
}
wg.Wait()
}

View file

@ -7,16 +7,3 @@ import (
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
}

View file

@ -36,6 +36,8 @@ func getModuleVersion(modulePath string) (string, error) {
}
func CopyAssets() {
dirutil.CreateHtmgoDir()
moduleName := "github.com/maddalax/htmgo/framework"
modulePath := module.GetDependencyPath(moduleName)
@ -90,7 +92,7 @@ func CopyAssets() {
})
}
if !dirutil.HasFileFromRoot("tailwind.config.js") {
if dirutil.GetConfig().Tailwind && !dirutil.HasFileFromRoot("tailwind.config.js") {
err = dirutil.CopyFile(
filepath.Join(assetCssDir, "tailwind.config.js"),
filepath.Join(process.GetWorkingDir(), "tailwind.config.js"),

View file

@ -12,7 +12,7 @@ import (
)
func IsTailwindEnabled() bool {
return dirutil.HasFileFromRoot("tailwind.config.js")
return dirutil.GetConfig().Tailwind && dirutil.HasFileFromRoot("tailwind.config.js")
}
func Setup() bool {
@ -78,7 +78,7 @@ func downloadTailwindCli() {
log.Fatal(fmt.Sprintf("Unsupported OS/ARCH: %s/%s", os, arch))
}
fileName := fmt.Sprintf(`tailwindcss-%s`, distro)
url := fmt.Sprintf(`https://github.com/tailwindlabs/tailwindcss/releases/latest/download/%s`, fileName)
url := fmt.Sprintf(`https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.16/%s`, fileName)
cmd := fmt.Sprintf(`curl -LO %s`, url)
process.Run(process.NewRawCommand("tailwind-cli-download", cmd, process.ExitOnError))

View file

@ -69,8 +69,6 @@ func DownloadTemplate(outPath string) {
slog.Debug("current working dir", slog.String("cwd", process.GetWorkingDir()))
commands := [][]string{
{"go", "get", "github.com/maddalax/htmgo/framework@latest"},
{"go", "get", "github.com/maddalax/htmgo/framework-ui@latest"},
{"git", "init"},
}

View file

@ -0,0 +1,50 @@
package formatter
import (
"fmt"
"github.com/maddalax/htmgo/tools/html-to-htmgo/htmltogo"
"os"
"path/filepath"
"strings"
)
func FormatDir(dir string) {
files, err := os.ReadDir(dir)
if err != nil {
fmt.Printf("error reading dir: %s\n", err.Error())
return
}
for _, file := range files {
if file.IsDir() {
FormatDir(filepath.Join(dir, file.Name()))
} else {
FormatFile(filepath.Join(dir, file.Name()))
}
}
}
func FormatFile(file string) {
if !strings.HasSuffix(file, ".go") {
return
}
fmt.Printf("formatting file: %s\n", file)
source, err := os.ReadFile(file)
if err != nil {
fmt.Printf("error reading file: %s\n", err.Error())
return
}
str := string(source)
if !strings.Contains(str, "github.com/maddalax/htmgo/framework/h") {
return
}
parsed := htmltogo.Indent(str)
os.WriteFile(file, []byte(parsed), 0644)
return
}

View file

@ -12,7 +12,10 @@ func KillProcess(process CmdWithFlags) error {
if process.Cmd == nil || process.Cmd.Process == nil {
return nil
}
Run(NewRawCommand("killprocess", fmt.Sprintf("taskkill /F /T /PID %s", strconv.Itoa(process.Cmd.Process.Pid))))
err := exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(process.Cmd.Process.Pid)).Run()
if err != nil {
fmt.Println(err)
}
time.Sleep(time.Millisecond * 50)
return nil
}

View file

@ -115,7 +115,7 @@ func OnShutdown() {
}
}
// give it a second
time.Sleep(time.Second * 2)
time.Sleep(time.Second * 1)
// force kill
KillAll()
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/maddalax/htmgo/cli/htmgo/tasks/astgen"
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
"github.com/maddalax/htmgo/cli/htmgo/tasks/css"
"github.com/maddalax/htmgo/cli/htmgo/tasks/run"
"github.com/maddalax/htmgo/cli/htmgo/tasks/util"
@ -101,6 +102,17 @@ func OnFileChange(version string, events []*fsnotify.Event) {
hasTask = true
}
// framework assets changed
if c.HasAnySuffix("assets/dist/htmgo.js") {
copyassets.CopyAssets()
//tasks.Run = true
}
// something in public folder changed
if c.HasAnyPrefix("assets/public/") {
copyassets.CopyAssets()
}
if hasTask {
slog.Info("file changed", slog.String("version", version), slog.String("file", c.Name()))
}

View file

@ -9,12 +9,23 @@ import (
"os"
)
func Build() {
func MakeBuildable() {
copyassets.CopyAssets()
astgen.GenAst(process.ExitOnError)
css.GenerateCss(process.ExitOnError)
astgen.GenAst(process.ExitOnError)
}
process.RunOrExit(process.NewRawCommand("", "mkdir -p ./dist"))
func Build() {
MakeBuildable()
_ = os.RemoveAll("./dist")
err := os.Mkdir("./dist", 0755)
if err != nil {
fmt.Println("Error creating dist directory", err)
os.Exit(1)
}
if os.Getenv("SKIP_GO_BUILD") != "1" {
process.RunOrExit(process.NewRawCommand("", fmt.Sprintf("go build -tags prod -o ./dist")))

View file

@ -1,7 +1,42 @@
package run
import "github.com/maddalax/htmgo/cli/htmgo/tasks/process"
import (
"fmt"
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
"io/fs"
"os"
"path/filepath"
)
func Server(flags ...process.RunFlag) error {
return process.Run(process.NewRawCommand("run-server", "go run .", flags...))
buildDir := "./__htmgo/temp-build"
_ = os.RemoveAll(buildDir)
err := os.Mkdir(buildDir, 0755)
if err != nil {
return err
}
process.RunOrExit(process.NewRawCommand("", fmt.Sprintf("go build -o %s", buildDir)))
binaryPath := ""
// find the binary that was built
err = filepath.WalkDir(buildDir, func(path string, d fs.DirEntry, err error) error {
if d.IsDir() {
return nil
}
binaryPath = path
return nil
})
if err != nil {
return err
}
if binaryPath == "" {
return fmt.Errorf("could not find the binary")
}
return process.Run(process.NewRawCommand("run-server", fmt.Sprintf("./%s", binaryPath), flags...))
}

View file

@ -1,19 +1,12 @@
package run
import (
"github.com/maddalax/htmgo/cli/htmgo/tasks/astgen"
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
"github.com/maddalax/htmgo/cli/htmgo/tasks/css"
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
)
func Setup() {
process.RunOrExit(process.NewRawCommand("", "go mod download"))
process.RunOrExit(process.NewRawCommand("", "go mod tidy"))
copyassets.CopyAssets()
astgen.GenAst(process.ExitOnError)
css.GenerateCss(process.ExitOnError)
MakeBuildable()
EntGenerate()
}

View file

@ -19,7 +19,7 @@ func ReplaceTextInFile(file string, text string, replacement string) error {
func ReplaceTextInDirRecursive(dir string, text string, replacement string, filter func(file string) bool) error {
return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if filter(path) {
if filter(filepath.Base(path)) {
_ = ReplaceTextInFile(path, text, replacement)
}
return nil

View file

@ -4,18 +4,20 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/google/uuid"
"github.com/maddalax/htmgo/cli/htmgo/internal"
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
"github.com/maddalax/htmgo/cli/htmgo/tasks/module"
"log"
"log/slog"
"os"
"path/filepath"
"strings"
"time"
)
var ignoredDirs = []string{".git", ".idea", "node_modules", "vendor"}
func startWatcher(cb func(version string, file []*fsnotify.Event)) {
events := make([]*fsnotify.Event, 0)
debouncer := internal.NewDebouncer(500 * time.Millisecond)
config := dirutil.GetConfig()
defer func() {
if r := recover(); r != nil {
@ -36,7 +38,38 @@ func startWatcher(cb func(version string, file []*fsnotify.Event)) {
if !ok {
return
}
if event.Has(fsnotify.Remove) {
if dirutil.IsGlobMatch(event.Name, config.WatchFiles, config.WatchIgnore) {
watcher.Remove(event.Name)
continue
}
}
if event.Has(fsnotify.Create) {
if dirutil.IsGlobMatch(event.Name, config.WatchFiles, config.WatchIgnore) {
watcher.Add(event.Name)
continue
}
info, err := os.Stat(event.Name)
if err != nil {
slog.Error("Error getting file info:", slog.String("path", event.Name), slog.String("error", err.Error()))
continue
}
if info.IsDir() {
err = watcher.Add(event.Name)
if err != nil {
slog.Error("Error adding directory to watcher:", slog.String("path", event.Name), slog.String("error", err.Error()))
} else {
slog.Debug("Watching directory:", slog.String("path", event.Name))
}
}
}
if event.Has(fsnotify.Write) || event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) {
if !dirutil.IsGlobMatch(event.Name, config.WatchFiles, config.WatchIgnore) {
continue
}
events = append(events, &event)
debouncer.Do(func() {
seen := make(map[string]bool)
@ -51,38 +84,48 @@ func startWatcher(cb func(version string, file []*fsnotify.Event)) {
events = make([]*fsnotify.Event, 0)
})
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
slog.Error("error:", err.Error())
slog.Error("error:", slog.String("error", err.Error()))
}
}
}()
rootDir := "."
frameworkPath := module.GetDependencyPath("github.com/maddalax/htmgo/framework")
if !strings.HasPrefix(frameworkPath, "github.com/") {
assetPath := filepath.Join(frameworkPath, "assets", "dist")
slog.Debug("Watching directory:", slog.String("path", assetPath))
watcher.Add(assetPath)
}
// Walk through the root directory and add all subdirectories to the watcher
err = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Ignore directories in the ignoredDirs list
for _, ignoredDir := range ignoredDirs {
if ignoredDir == info.Name() {
if dirutil.IsGlobExclude(path, config.WatchIgnore) {
return filepath.SkipDir
}
}
// Only watch directories
if info.IsDir() {
err = watcher.Add(path)
if err != nil {
slog.Error("Error adding directory to watcher:", err)
slog.Error("Error adding directory to watcher:", slog.String("error", err.Error()))
} else {
slog.Debug("Watching directory:", slog.String("path", path))
}
}
return nil
})
if err != nil {
log.Fatal(err)
}

View file

@ -0,0 +1,11 @@
# Project exclude paths
/tmp/
node_modules/
dist/
js/dist
js/node_modules
go.work
go.work.sum
.idea
!framework/assets/dist
__htmgo

6
examples/chat/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/assets/dist
tmp
node_modules
.idea
__htmgo
dist

36
examples/chat/Dockerfile Normal file
View file

@ -0,0 +1,36 @@
# Stage 1: Build the Go binary
FROM golang:1.23 AS builder
# Set the working directory inside the container
WORKDIR /app
# Copy go.mod and go.sum files
COPY go.mod go.sum ./
# Download and cache the Go modules
RUN go mod download
# Copy the source code into the container
COPY . .
# Build the Go binary for Linux
RUN CGO_ENABLED=0 GOPRIVATE=github.com/maddalax LOG_LEVEL=debug go run github.com/maddalax/htmgo/cli/htmgo@latest build
RUN CGO_ENABLED=1 GOOS=linux go build -tags prod -o ./dist -a -ldflags '-linkmode external -extldflags "-static"' .
# Stage 2: Create the smallest possible image
FROM gcr.io/distroless/base-debian11
# Set the working directory inside the container
WORKDIR /app
# Copy the Go binary from the builder stage
COPY --from=builder /app/dist .
# Expose the necessary port (replace with your server port)
EXPOSE 3000
# Command to run the binary
CMD ["./chat"]

View file

@ -0,0 +1,20 @@
version: '3'
tasks:
run:
cmds:
- go run github.com/maddalax/htmgo/cli/htmgo@latest run
silent: true
build:
cmds:
- go run github.com/maddalax/htmgo/cli/htmgo@latest build
docker:
cmds:
- docker build .
watch:
cmds:
- go run github.com/maddalax/htmgo/cli/htmgo@latest watch
silent: true

13
examples/chat/assets.go Normal file
View file

@ -0,0 +1,13 @@
//go:build !prod
// +build !prod
package main
import (
"chat/internal/embedded"
"io/fs"
)
func GetStaticAssets() fs.FS {
return embedded.NewOsFs()
}

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,16 @@
//go:build prod
// +build prod
package main
import (
"embed"
"io/fs"
)
//go:embed assets/dist/*
var staticAssets embed.FS
func GetStaticAssets() fs.FS {
return staticAssets
}

View file

@ -0,0 +1,155 @@
package chat
import (
"chat/internal/db"
"chat/sse"
"context"
"fmt"
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/service"
"time"
)
type Manager struct {
socketManager *sse.SocketManager
queries *db.Queries
service *Service
}
func NewManager(locator *service.Locator) *Manager {
return &Manager{
socketManager: service.Get[sse.SocketManager](locator),
queries: service.Get[db.Queries](locator),
service: NewService(locator),
}
}
func (m *Manager) StartListener() {
c := make(chan sse.SocketEvent, 1)
m.socketManager.Listen(c)
for {
select {
case event := <-c:
switch event.Type {
case sse.ConnectedEvent:
m.OnConnected(event)
case sse.DisconnectedEvent:
m.OnDisconnected(event)
case sse.MessageEvent:
m.onMessage(event)
default:
fmt.Printf("Unknown event type: %s\n", event.Type)
}
}
}
}
func (m *Manager) dispatchConnectedUsers(roomId string, predicate func(conn sse.SocketConnection) bool) {
connectedUsers := make([]db.User, 0)
// backfill all existing clients to the connected client
m.socketManager.ForEachSocket(roomId, func(conn sse.SocketConnection) {
if !predicate(conn) {
return
}
user, err := m.queries.GetUserBySessionId(context.Background(), conn.Id)
if err != nil {
return
}
connectedUsers = append(connectedUsers, user)
})
m.socketManager.ForEachSocket(roomId, func(conn sse.SocketConnection) {
m.socketManager.SendText(conn.Id, h.Render(ConnectedUsers(connectedUsers, conn.Id)))
})
}
func (m *Manager) OnConnected(e sse.SocketEvent) {
room, _ := m.service.GetRoom(e.RoomId)
if room == nil {
m.socketManager.CloseWithMessage(e.Id, "invalid room")
return
}
user, err := m.queries.GetUserBySessionId(context.Background(), e.Id)
if err != nil {
m.socketManager.CloseWithMessage(e.Id, "invalid user")
return
}
fmt.Printf("User %s connected to %s\n", user.Name, e.RoomId)
m.dispatchConnectedUsers(e.RoomId, func(conn sse.SocketConnection) bool {
return true
})
m.backFill(e.Id, e.RoomId)
}
func (m *Manager) OnDisconnected(e sse.SocketEvent) {
user, err := m.queries.GetUserBySessionId(context.Background(), e.Id)
if err != nil {
return
}
room, err := m.service.GetRoom(e.RoomId)
if err != nil {
return
}
fmt.Printf("User %s disconnected from %s\n", user.Name, room.ID)
m.dispatchConnectedUsers(e.RoomId, func(conn sse.SocketConnection) bool {
return conn.Id != e.Id
})
}
func (m *Manager) backFill(socketId string, roomId string) {
messages, _ := m.queries.GetLastMessages(context.Background(), db.GetLastMessagesParams{
ChatRoomID: roomId,
Limit: 200,
})
for _, message := range messages {
parsed, _ := time.Parse("2006-01-02 15:04:05", message.CreatedAt)
m.socketManager.SendText(socketId,
h.Render(MessageRow(&Message{
UserId: message.UserID,
UserName: message.UserName,
Message: message.Message,
CreatedAt: parsed,
})),
)
}
}
func (m *Manager) onMessage(e sse.SocketEvent) {
message := e.Payload["message"].(string)
if message == "" {
return
}
user, err := m.queries.GetUserBySessionId(context.Background(), e.Id)
if err != nil {
fmt.Printf("Error getting user: %v\n", err)
return
}
saved := m.service.InsertMessage(
&user,
e.RoomId,
message,
)
if saved != nil {
m.socketManager.BroadcastText(
e.RoomId,
h.Render(MessageRow(saved)),
func(conn sse.SocketConnection) bool {
return true
},
)
}
}

View file

@ -0,0 +1,58 @@
package chat
import (
"chat/internal/db"
"fmt"
"github.com/maddalax/htmgo/framework/h"
"strings"
"time"
)
func MessageRow(message *Message) *h.Element {
return h.Div(
h.Attribute("hx-swap-oob", "beforeend"),
h.Class("flex flex-col gap-4 w-full break-words whitespace-normal"),
// Ensure container breaks long words
h.Id("messages"),
h.Div(
h.Class("flex flex-col gap-1"),
h.Div(
h.Class("flex gap-2 items-center"),
h.Pf(
message.UserName,
h.Class("font-bold"),
),
h.Pf(message.CreatedAt.In(time.Local).Format("01/02 03:04 PM")),
),
h.Article(
h.Class("break-words whitespace-normal"),
// Ensure message text wraps correctly
h.P(
h.Text(message.Message),
),
),
),
)
}
func ConnectedUsers(users []db.User, myId string) *h.Element {
return h.Ul(
h.Attribute("hx-swap-oob", "outerHTML"),
h.Id("connected-users"),
h.Class("flex flex-col"),
h.List(users, func(user db.User, index int) *h.Element {
return connectedUser(user.Name, user.SessionID == myId)
}),
)
}
func connectedUser(username string, isMe bool) *h.Element {
id := fmt.Sprintf("connected-user-%s", strings.ReplaceAll(username, "#", "-"))
return h.Li(
h.Id(id),
h.ClassX("truncate text-slate-700", h.ClassMap{
"font-bold": isMe,
}),
h.Text(username),
)
}

View file

@ -0,0 +1,84 @@
package chat
import (
"chat/internal/db"
"context"
"fmt"
"github.com/google/uuid"
"github.com/maddalax/htmgo/framework/service"
"log"
"time"
)
type Message struct {
UserId int64 `json:"userId"`
UserName string `json:"userName"`
Message string `json:"message"`
CreatedAt time.Time `json:"createdAt"`
}
type Service struct {
queries *db.Queries
}
func NewService(locator *service.Locator) *Service {
return &Service{
queries: service.Get[db.Queries](locator),
}
}
func (s *Service) InsertMessage(user *db.User, roomId string, message string) *Message {
err := s.queries.InsertMessage(context.Background(), db.InsertMessageParams{
UserID: user.ID,
Username: user.Name,
ChatRoomID: roomId,
Message: message,
})
if err != nil {
log.Printf("Failed to insert message: %v\n", err)
return nil
}
return &Message{
UserId: user.ID,
UserName: user.Name,
Message: message,
CreatedAt: time.Now(),
}
}
func (s *Service) GetUserBySession(sessionId string) (*db.User, error) {
user, err := s.queries.GetUserBySessionId(context.Background(), sessionId)
return &user, err
}
func (s *Service) CreateUser(name string) (*db.CreateUserRow, error) {
nameWithHash := fmt.Sprintf("%s#%s", name, uuid.NewString()[0:4])
sessionId := fmt.Sprintf("session-%s-%s", uuid.NewString(), uuid.NewString())
user, err := s.queries.CreateUser(context.Background(), db.CreateUserParams{
Name: nameWithHash,
SessionID: sessionId,
})
if err != nil {
return nil, err
}
return &user, nil
}
func (s *Service) CreateRoom(name string) (*db.CreateChatRoomRow, error) {
room, err := s.queries.CreateChatRoom(context.Background(), db.CreateChatRoomParams{
ID: fmt.Sprintf("room-%s-%s", uuid.NewString()[0:8], name),
Name: name,
})
if err != nil {
return nil, err
}
return &room, nil
}
func (s *Service) GetRoom(id string) (*db.ChatRoom, error) {
room, err := s.queries.GetChatRoom(context.Background(), id)
if err != nil {
return nil, err
}
return &room, nil
}

View file

@ -0,0 +1,57 @@
package components
import "github.com/maddalax/htmgo/framework/h"
type ButtonProps struct {
Id string
Text string
Target string
Type string
Trigger string
Get string
Class string
Children []h.Ren
}
func PrimaryButton(props ButtonProps) h.Ren {
props.Class = h.MergeClasses(props.Class, "border-slate-800 bg-slate-900 hover:bg-slate-800 text-white")
return Button(props)
}
func SecondaryButton(props ButtonProps) h.Ren {
props.Class = h.MergeClasses(props.Class, "border-gray-700 bg-gray-700 text-white")
return Button(props)
}
func Button(props ButtonProps) h.Ren {
text := h.Text(props.Text)
button := h.Button(
h.If(
props.Id != "",
h.Id(props.Id),
),
h.If(
props.Children != nil,
h.Children(props.Children...),
),
h.Class("flex gap-1 items-center justify-center border p-4 rounded cursor-hover", props.Class),
h.If(
props.Get != "",
h.Get(props.Get),
),
h.If(
props.Target != "",
h.HxTarget(props.Target),
),
h.IfElse(
props.Type != "",
h.Type(props.Type),
h.Type("button"),
),
text,
)
return button
}

View file

@ -0,0 +1,14 @@
package components
import "github.com/maddalax/htmgo/framework/h"
func FormError(error string) *h.Element {
return h.Div(
h.Id("form-error"),
h.Text(error),
h.If(
error != "",
h.Class("p-4 bg-rose-400 text-white rounded"),
),
)
}

View file

@ -0,0 +1,81 @@
package components
import (
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/hx"
)
type InputProps struct {
Id string
Label string
Name string
Type string
DefaultValue string
Placeholder string
Required bool
ValidationPath string
Error string
Children []h.Ren
}
func Input(props InputProps) *h.Element {
validation := h.If(
props.ValidationPath != "",
h.Children(
h.Post(props.ValidationPath, hx.BlurEvent),
h.Attribute("hx-swap", "innerHTML transition:true"),
h.Attribute("hx-target", "next div"),
),
)
if props.Type == "" {
props.Type = "text"
}
input := h.Input(
props.Type,
h.Class("border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"),
h.If(
props.Name != "",
h.Name(props.Name),
),
h.If(
props.Children != nil,
h.Children(props.Children...),
),
h.If(
props.Required,
h.Required(),
),
h.If(
props.Placeholder != "",
h.Placeholder(props.Placeholder),
),
h.If(
props.DefaultValue != "",
h.Attribute("value", props.DefaultValue),
),
validation,
)
wrapped := h.Div(
h.If(
props.Id != "",
h.Id(props.Id),
),
h.Class("flex flex-col gap-1"),
h.If(
props.Label != "",
h.Label(
h.Text(props.Label),
),
),
input,
h.Div(
h.Id(props.Id+"-error"),
h.Class("text-red-500"),
),
)
return wrapped
}

11
examples/chat/go.mod Normal file
View file

@ -0,0 +1,11 @@
module chat
go 1.23.0
require (
github.com/go-chi/chi/v5 v5.1.0
github.com/google/uuid v1.6.0
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
github.com/mattn/go-sqlite3 v1.14.23
github.com/puzpuzpuz/xsync/v3 v3.4.0
)

20
examples/chat/go.sum Normal file
View file

@ -0,0 +1,20 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
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/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package db
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View file

@ -0,0 +1,35 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package db
import (
"database/sql"
)
type ChatRoom struct {
ID string
Name string
LastMessageSentAt sql.NullString
CreatedAt string
UpdatedAt string
}
type Message struct {
ID int64
ChatRoomID string
UserID int64
Username string
Message string
CreatedAt string
UpdatedAt string
}
type User struct {
ID int64
Name string
CreatedAt string
UpdatedAt string
SessionID string
}

View file

@ -0,0 +1,25 @@
package db
import (
"context"
"database/sql"
_ "embed"
_ "github.com/mattn/go-sqlite3"
)
//go:embed schema.sql
var ddl string
func Provide() *Queries {
db, err := sql.Open("sqlite3", "file:chat.db?cache=shared&_fk=1")
if err != nil {
panic(err)
}
if _, err := db.ExecContext(context.Background(), ddl); err != nil {
panic(err)
}
return New(db)
}

View file

@ -0,0 +1,47 @@
-- name: CreateChatRoom :one
INSERT INTO chat_rooms (id, name, created_at, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, name, created_at, updated_at, last_message_sent_at;
-- name: InsertMessage :exec
INSERT INTO messages (chat_room_id, user_id, username, message, created_at, updated_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, chat_room_id, user_id, username, message, created_at, updated_at;
-- name: UpdateChatRoomLastMessageSentAt :exec
UPDATE chat_rooms
SET last_message_sent_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE id = ?;
-- name: GetChatRoom :one
SELECT
id,
name,
last_message_sent_at,
created_at,
updated_at
FROM chat_rooms
WHERE chat_rooms.id = ?;
-- name: CreateUser :one
INSERT INTO users (name, session_id, created_at, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, name, session_id, created_at, updated_at;
-- name: GetLastMessages :many
SELECT
messages.id,
messages.chat_room_id,
messages.user_id,
users.name AS user_name,
messages.message,
messages.created_at,
messages.updated_at
FROM messages
JOIN users ON messages.user_id = users.id
WHERE messages.chat_room_id = ?
ORDER BY messages.created_at
LIMIT ?;
-- name: GetUserBySessionId :one
SELECT * FROM users WHERE session_id = ?;

View file

@ -0,0 +1,212 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: queries.sql
package db
import (
"context"
"database/sql"
)
const createChatRoom = `-- name: CreateChatRoom :one
INSERT INTO chat_rooms (id, name, created_at, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, name, created_at, updated_at, last_message_sent_at
`
type CreateChatRoomParams struct {
ID string
Name string
}
type CreateChatRoomRow struct {
ID string
Name string
CreatedAt string
UpdatedAt string
LastMessageSentAt sql.NullString
}
func (q *Queries) CreateChatRoom(ctx context.Context, arg CreateChatRoomParams) (CreateChatRoomRow, error) {
row := q.db.QueryRowContext(ctx, createChatRoom, arg.ID, arg.Name)
var i CreateChatRoomRow
err := row.Scan(
&i.ID,
&i.Name,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastMessageSentAt,
)
return i, err
}
const createUser = `-- name: CreateUser :one
INSERT INTO users (name, session_id, created_at, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, name, session_id, created_at, updated_at
`
type CreateUserParams struct {
Name string
SessionID string
}
type CreateUserRow struct {
ID int64
Name string
SessionID string
CreatedAt string
UpdatedAt string
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
row := q.db.QueryRowContext(ctx, createUser, arg.Name, arg.SessionID)
var i CreateUserRow
err := row.Scan(
&i.ID,
&i.Name,
&i.SessionID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getChatRoom = `-- name: GetChatRoom :one
SELECT
id,
name,
last_message_sent_at,
created_at,
updated_at
FROM chat_rooms
WHERE chat_rooms.id = ?
`
func (q *Queries) GetChatRoom(ctx context.Context, id string) (ChatRoom, error) {
row := q.db.QueryRowContext(ctx, getChatRoom, id)
var i ChatRoom
err := row.Scan(
&i.ID,
&i.Name,
&i.LastMessageSentAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getLastMessages = `-- name: GetLastMessages :many
SELECT
messages.id,
messages.chat_room_id,
messages.user_id,
users.name AS user_name,
messages.message,
messages.created_at,
messages.updated_at
FROM messages
JOIN users ON messages.user_id = users.id
WHERE messages.chat_room_id = ?
ORDER BY messages.created_at
LIMIT ?
`
type GetLastMessagesParams struct {
ChatRoomID string
Limit int64
}
type GetLastMessagesRow struct {
ID int64
ChatRoomID string
UserID int64
UserName string
Message string
CreatedAt string
UpdatedAt string
}
func (q *Queries) GetLastMessages(ctx context.Context, arg GetLastMessagesParams) ([]GetLastMessagesRow, error) {
rows, err := q.db.QueryContext(ctx, getLastMessages, arg.ChatRoomID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetLastMessagesRow
for rows.Next() {
var i GetLastMessagesRow
if err := rows.Scan(
&i.ID,
&i.ChatRoomID,
&i.UserID,
&i.UserName,
&i.Message,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserBySessionId = `-- name: GetUserBySessionId :one
SELECT id, name, created_at, updated_at, session_id FROM users WHERE session_id = ?
`
func (q *Queries) GetUserBySessionId(ctx context.Context, sessionID string) (User, error) {
row := q.db.QueryRowContext(ctx, getUserBySessionId, sessionID)
var i User
err := row.Scan(
&i.ID,
&i.Name,
&i.CreatedAt,
&i.UpdatedAt,
&i.SessionID,
)
return i, err
}
const insertMessage = `-- name: InsertMessage :exec
INSERT INTO messages (chat_room_id, user_id, username, message, created_at, updated_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, chat_room_id, user_id, username, message, created_at, updated_at
`
type InsertMessageParams struct {
ChatRoomID string
UserID int64
Username string
Message string
}
func (q *Queries) InsertMessage(ctx context.Context, arg InsertMessageParams) error {
_, err := q.db.ExecContext(ctx, insertMessage,
arg.ChatRoomID,
arg.UserID,
arg.Username,
arg.Message,
)
return err
}
const updateChatRoomLastMessageSentAt = `-- name: UpdateChatRoomLastMessageSentAt :exec
UPDATE chat_rooms
SET last_message_sent_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`
func (q *Queries) UpdateChatRoomLastMessageSentAt(ctx context.Context, id string) error {
_, err := q.db.ExecContext(ctx, updateChatRoomLastMessageSentAt, id)
return err
}

View file

@ -0,0 +1,33 @@
CREATE TABLE IF NOT EXISTS users
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
session_id TEXT NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS chat_rooms
(
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
last_message_sent_at TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) STRICT;
CREATE TABLE IF NOT EXISTS messages
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_room_id TEXT NOT NULL,
user_id INTEGER NOT NULL,
username TEXT NOT NULL,
message TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (chat_room_id) REFERENCES chat_rooms (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
) STRICT;
CREATE INDEX IF NOT EXISTS idx_messages_chat_room_id ON messages (chat_room_id);
CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages (user_id);

View file

@ -0,0 +1,17 @@
package embedded
import (
"io/fs"
"os"
)
type OsFs struct {
}
func (receiver OsFs) Open(name string) (fs.File, error) {
return os.Open(name)
}
func NewOsFs() OsFs {
return OsFs{}
}

View file

@ -0,0 +1,25 @@
package routine
import (
"fmt"
"time"
)
func DebugLongRunning(name string, f func()) {
now := time.Now()
done := make(chan struct{}, 1)
go func() {
ticker := time.NewTicker(time.Second * 5)
for {
select {
case <-done:
return
case <-ticker.C:
elapsed := time.Since(now).Milliseconds()
fmt.Printf("function %s has not finished after %dms\n", name, elapsed)
}
}
}()
f()
done <- struct{}{}
}

54
examples/chat/main.go Normal file
View file

@ -0,0 +1,54 @@
package main
import (
"chat/__htmgo"
"chat/chat"
"chat/internal/db"
"chat/sse"
"fmt"
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/service"
"io/fs"
"net/http"
"runtime"
"time"
)
func main() {
locator := service.NewLocator()
service.Set[db.Queries](locator, service.Singleton, db.Provide)
service.Set[sse.SocketManager](locator, service.Singleton, func() *sse.SocketManager {
return sse.NewSocketManager()
})
chatManager := chat.NewManager(locator)
go chatManager.StartListener()
go func() {
for {
count := runtime.NumGoroutine()
fmt.Printf("goroutines: %d\n", count)
time.Sleep(10 * time.Second)
}
}()
h.Start(h.AppOpts{
ServiceLocator: locator,
LiveReload: true,
Register: func(app *h.App) {
sub, err := fs.Sub(GetStaticAssets(), "assets/dist")
if err != nil {
panic(err)
}
http.FileServerFS(sub)
app.Router.Handle("/public/*", http.StripPrefix("/public", http.FileServerFS(sub)))
app.Router.Handle("/sse/chat/{id}", sse.Handle())
__htmgo.Register(app.Router)
},
})
}

View file

@ -0,0 +1,164 @@
package pages
import (
"chat/chat"
"chat/internal/db"
"chat/partials"
"fmt"
"github.com/go-chi/chi/v5"
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/js"
"time"
)
func ChatRoom(ctx *h.RequestContext) *h.Page {
roomId := chi.URLParam(ctx.Request, "id")
return h.NewPage(
RootPage(
h.Div(
h.TriggerChildren(),
h.Attribute("sse-connect", fmt.Sprintf("/sse/chat/%s", roomId)),
h.HxOnSseOpen(
js.ConsoleLog("Connected to chat room"),
),
h.HxOnSseError(
js.EvalJs(fmt.Sprintf(`
const reason = e.detail.event.data
if(['invalid room', 'no session', 'invalid user'].includes(reason)) {
window.location.href = '/?roomId=%s';
} else if(e.detail.event.code === 1011) {
window.location.reload()
} else if (e.detail.event.code === 1008 || e.detail.event.code === 1006) {
window.location.href = '/?roomId=%s';
} else {
console.error('Connection closed:', e.detail.event)
}
`, roomId, roomId)),
),
// Adjusted flex properties for responsive layout
h.Class("flex flex-row h-screen bg-neutral-100 overflow-x-hidden"),
// Collapse Button for mobile
CollapseButton(),
// Sidebar for connected users
UserSidebar(),
h.Div(
// Adjusted to fill height and width
h.Class("flex flex-col h-full w-full bg-white p-4 overflow-hidden"),
// Room name at the top, fixed
CachedRoomHeader(ctx),
h.HxAfterSseMessage(
js.EvalJsOnSibling("#messages",
`element.scrollTop = element.scrollHeight;`),
),
// Chat Messages
h.Div(
h.Id("messages"),
// Adjusted flex properties and removed max-width
h.Class("flex flex-col gap-4 mb-4 overflow-auto flex-grow w-full pt-[50px]"),
),
// Chat Input at the bottom
Form(),
),
),
),
)
}
var CachedRoomHeader = h.CachedPerKeyT(time.Hour, func(ctx *h.RequestContext) (string, h.GetElementFunc) {
roomId := chi.URLParam(ctx.Request, "id")
return roomId, func() *h.Element {
return roomNameHeader(ctx)
}
})
func roomNameHeader(ctx *h.RequestContext) *h.Element {
roomId := chi.URLParam(ctx.Request, "id")
service := chat.NewService(ctx.ServiceLocator())
room, err := service.GetRoom(roomId)
if err != nil {
return h.Div()
}
return h.Div(
h.Class("bg-neutral-700 text-white p-3 shadow-sm w-full fixed top-0 left-0 flex justify-center z-10"),
h.H2F(
room.Name,
h.Class("text-lg font-bold"),
),
h.Div(
h.Class("absolute right-5 top-3 cursor-pointer"),
h.Text("Share"),
h.OnClick(
js.EvalJs(`
alert("Share this url with your friends:\n " + window.location.href)
`),
),
),
)
}
func UserSidebar() *h.Element {
return h.Div(
h.Class("sidebar h-full pt-[67px] min-w-48 w-48 bg-neutral-200 p-4 flex-col justify-between gap-3 rounded-l-lg hidden md:flex"),
h.Div(
h.H3F(
"Connected Users",
h.Class("text-lg font-bold"),
),
chat.ConnectedUsers(make([]db.User, 0), ""),
),
h.A(
h.Class("cursor-pointer"),
h.Href("/"),
h.Text("Leave Room"),
),
)
}
func CollapseButton() *h.Element {
return h.Div(
h.Class("fixed top-0 left-4 md:hidden z-50"),
// Always visible on mobile
h.Button(
h.Class("p-2 text-2xl bg-neutral-700 text-white rounded-md"),
// Styling the button
h.OnClick(
js.EvalJs(`
const sidebar = document.querySelector('.sidebar');
sidebar.classList.toggle('hidden');
sidebar.classList.toggle('flex');
`),
),
h.UnsafeRaw("&#9776;"),
// The icon for collapsing the sidebar
),
)
}
func MessageInput() *h.Element {
return h.Input(
"text",
h.Id("message-input"),
h.Required(),
h.Class("p-4 rounded-md border border-slate-200 w-full focus:outline-none focus:ring focus:ring-slate-200"),
h.Name("message"),
h.MaxLength(1000),
h.Placeholder("Type a message..."),
h.HxAfterSseMessage(
js.SetValue(""),
),
)
}
func Form() *h.Element {
return h.Div(
h.Class("flex gap-4 items-center"),
h.Form(
h.NoSwap(),
h.PostPartial(partials.SendMessage),
h.Attribute("ws-send", ""),
h.Class("flex flex-grow"),
MessageInput(),
),
)
}

View file

@ -0,0 +1,87 @@
package pages
import (
"chat/components"
"chat/partials"
"github.com/maddalax/htmgo/framework/h"
)
func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page {
return h.NewPage(
RootPage(
h.Div(
h.Class("flex flex-col items-center justify-center min-h-screen bg-neutral-100"),
h.Div(
h.Class("bg-white p-8 rounded-lg shadow-lg w-full max-w-md"),
h.H2F(
"htmgo chat",
h.Class("text-3xl font-bold text-center mb-6"),
),
h.Form(
h.Attribute("hx-swap", "none"),
h.PostPartial(partials.CreateOrJoinRoom),
h.Class("flex flex-col gap-6"),
// Username input at the top
components.Input(components.InputProps{
Id: "username",
Name: "username",
Label: "Username",
Required: true,
Children: []h.Ren{
h.Attribute("autocomplete", "off"),
h.MaxLength(15),
},
}),
// Single box for Create or Join a Chat Room
h.Div(
h.Class("p-4 border border-gray-300 rounded-md flex flex-col gap-6"),
// Create New Chat Room input
components.Input(components.InputProps{
Name: "new-chat-room",
Label: "Create a new chat room",
Placeholder: "Enter chat room name",
Children: []h.Ren{
h.Attribute("autocomplete", "off"),
h.MaxLength(20),
},
}),
// OR divider
h.Div(
h.Class("flex items-center justify-center gap-4"),
h.Div(
h.Class("border-t border-gray-300 flex-grow"),
),
h.P(
h.Text("OR"),
h.Class("text-gray-500"),
),
h.Div(
h.Class("border-t border-gray-300 flex-grow"),
),
),
// Join Chat Room input
components.Input(components.InputProps{
Id: "join-chat-room",
Name: "join-chat-room",
Label: "Join an existing chat room",
Placeholder: "Enter chat room ID",
DefaultValue: ctx.QueryParam("roomId"),
Children: []h.Ren{
h.Attribute("autocomplete", "off"),
h.MaxLength(100),
},
}),
),
// Error message
components.FormError(""),
// Submit button at the bottom
components.PrimaryButton(components.ButtonProps{
Type: "submit",
Text: "Submit",
}),
),
),
),
),
)
}

View file

@ -0,0 +1,26 @@
package pages
import (
"github.com/maddalax/htmgo/framework/h"
)
func RootPage(children ...h.Ren) h.Ren {
extensions := h.BaseExtensions()
return h.Html(
h.HxExtension(extensions),
h.Meta("viewport", "width=device-width, initial-scale=1"),
h.Meta("title", "htmgo chat example"),
h.Meta("charset", "utf-8"),
h.Meta("author", "htmgo"),
h.Head(
h.Link("/public/main.css", "stylesheet"),
h.Script("/public/htmgo.js"),
),
h.Body(
h.Div(
h.Class("flex flex-col gap-2 bg-white h-full"),
h.Fragment(children...),
),
),
)
}

View file

@ -0,0 +1,35 @@
package partials
import (
"chat/components"
"chat/sse"
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/service"
)
func SendMessage(ctx *h.RequestContext) *h.Partial {
locator := ctx.ServiceLocator()
socketManager := service.Get[sse.SocketManager](locator)
sessionCookie, err := ctx.Request.Cookie("session_id")
if err != nil {
return h.SwapPartial(ctx, components.FormError("Session not found"))
}
message := ctx.Request.FormValue("message")
if message == "" {
return h.SwapPartial(ctx, components.FormError("Message is required"))
}
if len(message) > 200 {
return h.SwapPartial(ctx, components.FormError("Message is too long"))
}
socketManager.OnMessage(sessionCookie.Value, map[string]any{
"message": message,
})
return h.EmptyPartial()
}

View file

@ -0,0 +1,73 @@
package partials
import (
"chat/chat"
"chat/components"
"github.com/maddalax/htmgo/framework/h"
"net/http"
"time"
)
func CreateOrJoinRoom(ctx *h.RequestContext) *h.Partial {
locator := ctx.ServiceLocator()
service := chat.NewService(locator)
chatRoomId := ctx.Request.FormValue("join-chat-room")
username := ctx.Request.FormValue("username")
if username == "" {
return h.SwapPartial(ctx, components.FormError("Username is required"))
}
if len(username) > 15 {
return h.SwapPartial(ctx, components.FormError("Username is too long"))
}
user, err := service.CreateUser(username)
if err != nil {
return h.SwapPartial(ctx, components.FormError("Failed to create user"))
}
var redirect = func(path string) *h.Partial {
cookie := &http.Cookie{
Name: "session_id",
Value: user.SessionID,
Path: "/",
Expires: time.Now().Add(24 * 30 * time.Hour),
}
return h.RedirectPartialWithHeaders(
path,
h.NewHeaders(
"Set-Cookie", cookie.String(),
),
)
}
if chatRoomId != "" {
room, _ := service.GetRoom(chatRoomId)
if room == nil {
return h.SwapPartial(ctx, components.FormError("Room not found"))
} else {
return redirect("/chat/" + chatRoomId)
}
}
chatRoomName := ctx.Request.FormValue("new-chat-room")
if len(chatRoomName) > 20 {
return h.SwapPartial(ctx, components.FormError("Chat room name is too long"))
}
if chatRoomName != "" {
room, _ := service.CreateRoom(chatRoomName)
if room == nil {
return h.SwapPartial(ctx, components.FormError("Failed to create room"))
} else {
return redirect("/chat/" + room.ID)
}
}
return h.SwapPartial(ctx, components.FormError("Create a new room or join an existing one"))
}

9
examples/chat/sqlc.yaml Normal file
View file

@ -0,0 +1,9 @@
version: "2"
sql:
- schema: "internal/db/schema.sql"
queries: "internal/db/queries.sql"
engine: "sqlite"
gen:
go:
package: "db"
out: "internal/db"

View file

@ -0,0 +1,111 @@
package sse
import (
"fmt"
"github.com/go-chi/chi/v5"
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/service"
"log/slog"
"net/http"
"sync"
"time"
)
func Handle() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Set the necessary headers
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*") // Optional for CORS
cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext)
locator := cc.ServiceLocator()
manager := service.Get[SocketManager](locator)
sessionCookie, _ := r.Cookie("session_id")
sessionId := ""
if sessionCookie != nil {
sessionId = sessionCookie.Value
}
ctx := r.Context()
/*
Large buffer in case the client disconnects while we are writing
we don't want to block the writer
*/
done := make(chan bool, 1000)
writer := make(WriterChan, 1000)
wg := sync.WaitGroup{}
wg.Add(1)
/*
* This goroutine is responsible for writing messages to the client
*/
go func() {
defer wg.Done()
defer manager.Disconnect(sessionId)
defer func() {
for len(writer) > 0 {
<-writer
}
for len(done) > 0 {
<-done
}
}()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-done:
fmt.Printf("closing connection: \n")
return
case <-ticker.C:
manager.Ping(sessionId)
case message := <-writer:
_, err := fmt.Fprintf(w, message)
if err != nil {
done <- true
} else {
flusher, ok := w.(http.Flusher)
if ok {
flusher.Flush()
}
}
}
}
}()
/**
* This goroutine is responsible for adding the client to the room
*/
wg.Add(1)
go func() {
defer wg.Done()
if sessionId == "" {
manager.writeCloseRaw(writer, "no session")
return
}
roomId := chi.URLParam(r, "id")
if roomId == "" {
slog.Error("invalid room", slog.String("room_id", roomId))
manager.writeCloseRaw(writer, "invalid room")
return
}
manager.Add(roomId, sessionId, writer, done)
}()
wg.Wait()
}
}

View file

@ -0,0 +1,231 @@
package sse
import (
"chat/internal/routine"
"fmt"
"github.com/puzpuzpuz/xsync/v3"
"time"
)
type EventType string
type WriterChan chan string
type DoneChan chan bool
const (
ConnectedEvent EventType = "connected"
DisconnectedEvent EventType = "disconnected"
MessageEvent EventType = "message"
)
type SocketEvent struct {
Id string
RoomId string
Type EventType
Payload map[string]any
}
type CloseEvent struct {
Code int
Reason string
}
type SocketConnection struct {
Id string
RoomId string
Done DoneChan
Writer WriterChan
}
type SocketManager struct {
sockets *xsync.MapOf[string, *xsync.MapOf[string, SocketConnection]]
idToRoom *xsync.MapOf[string, string]
listeners []chan SocketEvent
}
func NewSocketManager() *SocketManager {
return &SocketManager{
sockets: xsync.NewMapOf[string, *xsync.MapOf[string, SocketConnection]](),
idToRoom: xsync.NewMapOf[string, string](),
}
}
func (manager *SocketManager) ForEachSocket(roomId string, cb func(conn SocketConnection)) {
sockets, ok := manager.sockets.Load(roomId)
if !ok {
return
}
sockets.Range(func(id string, conn SocketConnection) bool {
cb(conn)
return true
})
}
func (manager *SocketManager) Listen(listener chan SocketEvent) {
if manager.listeners == nil {
manager.listeners = make([]chan SocketEvent, 0)
}
if listener != nil {
manager.listeners = append(manager.listeners, listener)
}
}
func (manager *SocketManager) dispatch(event SocketEvent) {
done := make(chan struct{}, 1)
go func() {
for {
select {
case <-done:
return
case <-time.After(5 * time.Second):
fmt.Printf("havent dispatched listener event after 5s, chan blocked: %s\n", event.Type)
}
}
}()
for _, listener := range manager.listeners {
listener <- event
}
done <- struct{}{}
}
func (manager *SocketManager) OnMessage(id string, message map[string]any) {
socket := manager.Get(id)
if socket == nil {
return
}
manager.dispatch(SocketEvent{
Id: id,
Type: MessageEvent,
Payload: message,
RoomId: socket.RoomId,
})
}
func (manager *SocketManager) Add(roomId string, id string, writer WriterChan, done DoneChan) {
manager.idToRoom.Store(id, roomId)
sockets, ok := manager.sockets.LoadOrCompute(roomId, func() *xsync.MapOf[string, SocketConnection] {
return xsync.NewMapOf[string, SocketConnection]()
})
sockets.Store(id, SocketConnection{
Id: id,
Writer: writer,
RoomId: roomId,
Done: done,
})
s, ok := sockets.Load(id)
if !ok {
return
}
manager.dispatch(SocketEvent{
Id: s.Id,
Type: ConnectedEvent,
RoomId: s.RoomId,
Payload: map[string]any{},
})
fmt.Printf("User %s connected to %s\n", id, roomId)
}
func (manager *SocketManager) OnClose(id string) {
socket := manager.Get(id)
if socket == nil {
return
}
manager.dispatch(SocketEvent{
Id: id,
Type: DisconnectedEvent,
RoomId: socket.RoomId,
Payload: map[string]any{},
})
manager.sockets.Delete(id)
}
func (manager *SocketManager) CloseWithMessage(id string, message string) {
conn := manager.Get(id)
if conn != nil {
defer manager.OnClose(id)
manager.writeText(*conn, "error", message)
conn.Done <- true
}
}
func (manager *SocketManager) Disconnect(id string) {
conn := manager.Get(id)
if conn != nil {
manager.OnClose(id)
conn.Done <- true
}
}
func (manager *SocketManager) Get(id string) *SocketConnection {
roomId, ok := manager.idToRoom.Load(id)
if !ok {
return nil
}
sockets, ok := manager.sockets.Load(roomId)
if !ok {
return nil
}
conn, ok := sockets.Load(id)
return &conn
}
func (manager *SocketManager) Ping(id string) {
conn := manager.Get(id)
if conn != nil {
manager.writeText(*conn, "ping", "")
}
}
func (manager *SocketManager) writeCloseRaw(writer WriterChan, message string) {
manager.writeTextRaw(writer, "close", message)
}
func (manager *SocketManager) writeTextRaw(writer WriterChan, event string, message string) {
routine.DebugLongRunning("writeTextRaw", func() {
timeout := 3 * time.Second
data := ""
if event != "" {
data = fmt.Sprintf("event: %s\ndata: %s\n\n", event, message)
} else {
data = fmt.Sprintf("data: %s\n\n", message)
}
select {
case writer <- data:
case <-time.After(timeout):
fmt.Printf("could not send %s to channel after %s\n", data, timeout)
}
})
}
func (manager *SocketManager) writeText(socket SocketConnection, event string, message string) {
if socket.Writer == nil {
return
}
manager.writeTextRaw(socket.Writer, event, message)
}
func (manager *SocketManager) BroadcastText(roomId string, message string, predicate func(conn SocketConnection) bool) {
sockets, ok := manager.sockets.Load(roomId)
if !ok {
return
}
sockets.Range(func(id string, conn SocketConnection) bool {
if predicate(conn) {
manager.writeText(conn, "", message)
}
return true
})
}
func (manager *SocketManager) SendText(id string, message string) {
conn := manager.Get(id)
if conn != nil {
manager.writeText(*conn, "", message)
}
}

View file

@ -0,0 +1,5 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["**/*.go"],
plugins: [],
};

View file

@ -0,0 +1,11 @@
# Project exclude paths
/tmp/
node_modules/
dist/
js/dist
js/node_modules
go.work
go.work.sum
.idea
!framework/assets/dist
__htmgo

6
examples/hackernews/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/assets/dist
tmp
node_modules
.idea
__htmgo
dist

View file

@ -0,0 +1,38 @@
# Stage 1: Build the Go binary
FROM golang:1.23-alpine AS builder
RUN apk update
RUN apk add git
RUN apk add curl
# Set the working directory inside the container
WORKDIR /app
# Copy go.mod and go.sum files
COPY go.mod go.sum ./
# Download and cache the Go modules
RUN go mod download
# Copy the source code into the container
COPY . .
# Build the Go binary for Linux
RUN GOPRIVATE=github.com/maddalax GOPROXY=direct go run github.com/maddalax/htmgo/cli/htmgo@latest build
# Stage 2: Create the smallest possible image
FROM gcr.io/distroless/base-debian11
# Set the working directory inside the container
WORKDIR /app
# Copy the Go binary from the builder stage
COPY --from=builder /app/dist .
# Expose the necessary port (replace with your server port)
EXPOSE 3000
# Command to run the binary
CMD ["./hackernews"]

View file

@ -0,0 +1,20 @@
version: '3'
tasks:
run:
cmds:
- htmgo run
silent: true
build:
cmds:
- htmgo build
docker:
cmds:
- docker build .
watch:
cmds:
- htmgo watch
silent: true

View file

@ -0,0 +1,13 @@
//go:build !prod
// +build !prod
package main
import (
"hackernews/internal/embedded"
"io/fs"
)
func GetStaticAssets() fs.FS {
return embedded.NewOsFs()
}

View file

@ -0,0 +1,15 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
/* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -0,0 +1,16 @@
//go:build prod
// +build prod
package main
import (
"embed"
"io/fs"
)
//go:embed assets/dist/*
var staticAssets embed.FS
func GetStaticAssets() fs.FS {
return staticAssets
}

View file

@ -0,0 +1,14 @@
package components
import "github.com/maddalax/htmgo/framework/h"
func Badge(text string, active bool, children ...h.Ren) *h.Element {
return h.Button(
h.Text(text),
h.ClassX("font-semibold px-3 py-1 rounded-full cursor-pointer h-[32px]", h.ClassMap{
"bg-rose-500 text-white": active,
"bg-neutral-300": !active,
}),
h.Children(children...),
)
}

View file

@ -0,0 +1,16 @@
module hackernews
go 1.23.0
require (
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
github.com/microcosm-cc/bluemonday v1.0.27
)
require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/go-chi/chi/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
golang.org/x/net v0.29.0 // indirect
)

View file

@ -0,0 +1,22 @@
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
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/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,30 @@
package batch
import (
"sync"
)
func ParallelProcess[T any, Z any](items []T, concurrency int, cb func(item T) Z) []Z {
if len(items) == 0 {
return []Z{}
}
if len(items) == 1 {
return []Z{cb(items[0])}
}
results := make([]Z, len(items))
wg := sync.WaitGroup{}
sem := make(chan struct{}, concurrency)
for i, item := range items {
wg.Add(1)
sem <- struct{}{}
go func(item T) {
defer func() {
wg.Done()
<-sem
}()
results[i] = cb(item)
}(item)
}
wg.Wait()
return results
}

View file

@ -0,0 +1,17 @@
package embedded
import (
"io/fs"
"os"
)
type OsFs struct {
}
func (receiver OsFs) Open(name string) (fs.File, error) {
return os.Open(name)
}
func NewOsFs() OsFs {
return OsFs{}
}

View file

@ -0,0 +1,115 @@
package httpjson
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"time"
)
var (
client *http.Client
once sync.Once // Consider allowing configuration parameters for the singleton
)
func getClient() *http.Client {
once.Do(func() {
client = &http.Client{
Timeout: 10 * time.Second,
}
})
return client
}
func Get[T any](url string) (*T, error) {
client := getClient()
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var result T
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
return &result, nil
}
func Post[T any](url string, data T) (*http.Response, error) {
client := getClient()
body, err := json.Marshal(data)
if err != nil {
return nil, err
}
resp, err := client.Post(url, "application/json", bytes.NewBuffer(body))
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return resp, nil
}
func Patch[T any](url string, data T) error {
client := getClient()
body, err := json.Marshal(data)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return nil
}
func Delete(url string) error {
client := getClient()
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return nil
}

View file

@ -0,0 +1,152 @@
package news
import (
"fmt"
"github.com/maddalax/htmgo/framework/h"
"hackernews/internal/batch"
"hackernews/internal/httpjson"
"hackernews/internal/sanitize"
"hackernews/internal/timeformat"
"log/slog"
"strconv"
"time"
)
const baseUrl = "https://hacker-news.firebaseio.com/v0/"
func url(path string, qs *h.Qs) string {
return baseUrl + path + ".json?" + qs.ToString()
}
type Category struct {
Name string
Path string
}
var Categories = []Category{
{"Top Stories", "topstories"},
{"Best Stories", "beststories"},
{"New Stories", "newstories"},
}
type Comment struct {
By string `json:"by"`
Text string `json:"text"`
TimeRaw int64 `json:"time"`
Time time.Time `json:"-"`
Type string `json:"type"`
Kids []int `json:"kids"`
Parent int `json:"parent"`
Id int `json:"id"`
}
type Story struct {
Id int `json:"id"`
By string `json:"by"`
Text string `json:"text"`
Title string `json:"title"`
Type string `json:"type"`
Descendents int `json:"descendants"`
Score int `json:"score"`
Url string
TimeRaw int64 `json:"time"`
Time time.Time `json:"-"`
// comment ids
Kids []int
}
type GetTopStoriesRequest struct {
Limit int
Page int
}
func MustItemId(ctx *h.RequestContext) int {
raw := h.GetQueryParam(ctx, "item")
parsed, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return 0
}
return int(parsed)
}
func GetStories(category string, page int, limit int) []Story {
top, err := httpjson.Get[[]int](url(category, h.NewQs()))
if err != nil {
slog.Error("failed to load top stories", slog.String("err", err.Error()))
return make([]Story, 0)
}
ids := *top
start := page * limit
end := start + limit
if start > len(ids) {
return make([]Story, 0)
}
if end > len(ids) {
end = len(ids)
}
return batch.ParallelProcess[int, Story](
ids[start:end],
50,
func(id int) Story {
story, err := GetStory(id)
if err != nil {
slog.Error("failed to load story", slog.Int("id", id), slog.String("err", err.Error()))
return Story{}
}
return *story
},
)
}
func GetTopStories(page int, limit int) []Story {
return GetStories("topstories", page, limit)
}
func GetBestStories(page int, limit int) []Story {
return GetStories("beststories", page, limit)
}
func GetNewStories(page int, limit int) []Story {
return GetStories("newstories", page, limit)
}
func GetComments(ids []int) []Comment {
return batch.ParallelProcess(
ids,
50,
func(id int) Comment {
comment, err := GetComment(id)
if err != nil {
slog.Error("failed to load comment", slog.Int("id", id), slog.String("err", err.Error()))
return Comment{}
}
return *comment
},
)
}
func GetComment(id int) (*Comment, error) {
c, err := httpjson.Get[Comment](url(fmt.Sprintf("item/%d", id), h.NewQs()))
if err != nil {
return nil, err
}
c.Text = sanitize.Sanitize(c.Text)
c.By = sanitize.Sanitize(c.By)
c.Time = timeformat.ParseUnix(c.TimeRaw)
return c, nil
}
func GetStory(id int) (*Story, error) {
s, err := httpjson.Get[Story](url(fmt.Sprintf("item/%d", id), h.NewQs()))
if err != nil {
return nil, err
}
s.Title = sanitize.Sanitize(s.Title)
s.Text = sanitize.Sanitize(s.Text)
s.By = sanitize.Sanitize(s.By)
s.Time = timeformat.ParseUnix(s.TimeRaw)
return s, nil
}

View file

@ -0,0 +1,11 @@
package parse
import "strconv"
func MustParseInt(s string, fallback int) int {
v, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return fallback
}
return int(v)
}

Some files were not shown because too many files have changed in this diff Show more