Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From v0.20.0 To trunk
2025-03-25
| ||
16:57 | Adapt to zsc changes (refactored sz.ScanReference) ... (Leaf check-in: b2fddd6b32 user: stern tags: trunk) | |
2025-03-24
| ||
10:04 | Merge kernel/impl.go into kernel/kernel.go; remove some unneeded functions; make some functions private ... (check-in: 21fab08139 user: stern tags: trunk) | |
2025-03-07
| ||
16:48 | Fix release date for 0.20.0 in changelog ... (check-in: f0f37f254c user: stern tags: trunk) | |
16:06 | Version 0.20.0 ... (check-in: 674f1fe89d user: stern tags: trunk, release, v0.20.0) | |
10:41 | Update golang dependencies ... (check-in: b138cefd04 user: stern tags: trunk) | |
Changes to VERSION.
|
| | | 1 | 0.21.0-dev |
Deleted ast/ast.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted ast/block.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted ast/inline.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted ast/ref.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted ast/ref_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted ast/sztrans/sztrans.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted ast/walk.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted ast/walk_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/auth.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/cred/cred.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/impl/digest.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/impl/impl.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/policy/anon.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/policy/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/policy/default.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/policy/owner.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/policy/policy.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/policy/policy_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/policy/readonly.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/compbox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/config.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/keys.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/log.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/manager.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/memory.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/parser.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/sx.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/version.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/base.css.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/base.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/constbox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/contributors.zettel.
|
| < < < < < < < < |
Deleted box/constbox/delete.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/dependencies.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/emoji_spin.gif.
cannot compute difference between binary files
Deleted box/constbox/error.sxn.
|
| < < < < < < < < < < < < < < < < < |
Deleted box/constbox/form.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/home.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/info.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/license.txt.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/listzettel.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/login.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/menu_lists.zettel.
|
| < < < < < < < |
Deleted box/constbox/menu_new.zettel.
|
| < < < < < < |
Deleted box/constbox/prelude.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/roleconfiguration.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/rolerole.zettel.
|
| < < < < < < < < < < |
Deleted box/constbox/roletag.zettel.
|
| < < < < < < |
Deleted box/constbox/rolezettel.zettel.
|
| < < < < < < < |
Deleted box/constbox/start.sxn.
|
| < < < < < < < < < < < < < < < < < |
Deleted box/constbox/wuicode.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/zettel.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/dirbox/dirbox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/dirbox/dirbox_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/dirbox/service.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/filebox/filebox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/filebox/zipbox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/helper.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/anteroom.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/anteroom_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/collect.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/enrich.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/indexer.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/manager.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/mapstore/mapstore.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/store/store.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/store/wordset.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/store/wordset_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/store/zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/membox/membox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/directory.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/directory_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/entry.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/fsdir.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/helper.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/notify.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/simpledir.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to cmd/cmd_file.go.
︙ | ︙ | |||
19 20 21 22 23 24 25 | "fmt" "io" "os" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" | | > | | | | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | "fmt" "io" "os" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/zettel" ) // ---------- Subcommand: file ----------------------------------------------- func cmdFile(fs *flag.FlagSet) (int, error) { enc := fs.Lookup("t").Value.String() m, inp, err := getInput(fs.Args()) |
︙ | ︙ |
Changes to cmd/cmd_password.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 | "fmt" "os" "golang.org/x/term" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" | > | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | "fmt" "os" "golang.org/x/term" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth/cred" ) // ---------- Subcommand: password ------------------------------------------- func cmdPassword(fs *flag.FlagSet) (int, error) { if fs.NArg() == 0 { fmt.Fprintln(os.Stderr, "User name and user zettel identification missing") |
︙ | ︙ |
Changes to cmd/cmd_run.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | import ( "context" "flag" "net/http" "t73f.de/r/zsc/domain/meta" | > | | | | | | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import ( "context" "flag" "net/http" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter/api" "zettelstore.de/z/internal/web/adapter/webui" "zettelstore.de/z/internal/web/server" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { fs.String("c", "", "configuration file") fs.Uint("a", 0, "port number kernel service (0=disable)") |
︙ | ︙ |
Changes to cmd/command.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | package cmd import ( "flag" "maps" "slices" | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package cmd import ( "flag" "maps" "slices" "zettelstore.de/z/internal/logger" ) // Command stores information about commands / sub-commands. type Command struct { Name string // command name as it appears on the command line Func CommandFunc // function that executes a command Simple bool // Operate in simple-mode |
︙ | ︙ |
Changes to cmd/main.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( "crypto/sha256" "flag" "fmt" "net" "net/url" "os" "runtime/debug" "strconv" "strings" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" | > | > | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package cmd provides the commands to call Zettelstore from the command line. package cmd import ( "crypto/sha256" "flag" "fmt" "net" "net/url" "os" "runtime/debug" "strconv" "strings" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/auth/impl" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/compbox" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/web/server" ) const strRunSimple = "run-simple" func init() { RegisterCommand(Command{ Name: "help", |
︙ | ︙ |
Changes to cmd/register.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- | < | | | | | | < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd // Mention all needed boxes, encoders, and parsers to have them registered. import ( _ "zettelstore.de/z/internal/box/compbox" // Allow to use computed box. _ "zettelstore.de/z/internal/box/constbox" // Allow to use global internal box. _ "zettelstore.de/z/internal/box/dirbox" // Allow to use directory box. _ "zettelstore.de/z/internal/box/filebox" // Allow to use file box. _ "zettelstore.de/z/internal/box/membox" // Allow to use in-memory box. ) |
Deleted collect/collect.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted collect/collect_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted collect/order.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted config/config.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001007040310.zettel.
1 2 3 4 5 6 | id: 00001007040310 title: Zettelmarkup: Links role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007040310 title: Zettelmarkup: Links role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 modified: 20250313140836 There are two kinds of links, regardless of links to (internal) other zettel or to (external) material. Both kinds begin with two consecutive left square bracket characters (""''[''"", U+005B) and end with two consecutive right square bracket characters (""'']''"", U+005D). If the content starts with more than two left square bracket characters, all but the last two will be treated as text. The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", U+007C): ``[[text|linkspecification]]``. The text is a sequence of [[inline elements|00001007040000]]. |
︙ | ︙ | |||
32 33 34 35 36 37 38 | If a link specification begins with two slash characters (called __based reference__), it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]]. To specify some material outside the Zettelstore, just use a normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://tools.ietf.org/html/rfc3986]]. === Other topics If the link references another zettel, and this zettel is not readable for the current user, e.g. because of missing access rights, then only the associated text is presented. | > > > | 32 33 34 35 36 37 38 39 40 41 | If a link specification begins with two slash characters (called __based reference__), it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]]. To specify some material outside the Zettelstore, just use a normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://tools.ietf.org/html/rfc3986]]. === Other topics If the link references another zettel, and this zettel is not readable for the current user, e.g. because of missing access rights, then only the associated text is presented. A zettel reference to a non-existing zettel, i.e. using a legal zettel identifier that does not identify an existing zettel, the link text will be shown strikethrough. For example, ''[[Missing zettel|99999999999999]]'' will be rendered in HTML as ::[[Missing zettel|99999999999999]]::{=example}. |
Changes to docs/manual/00001012931400.zettel.
1 2 3 4 5 6 | id: 00001012931400 title: Encoding of Sz Block Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161803 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012931400 title: Encoding of Sz Block Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161803 modified: 20250317143910 === ''PARA'' :::syntax __Paragraph__ **=** ''(PARA'' [[__InlineElement__|00001012931600]] … '')''. ::: A paragraph is just a list of inline elements. |
︙ | ︙ | |||
27 28 29 30 31 32 33 | ::: === ''ORDERED'', ''UNORDERED'', ''QUOTATION'' These three symbols are specifying different kinds of lists / enumerations: an ordered list, an unordered list, and a quotation list. Their structure is the same. :::syntax | | | | | | | < < < < < < < | | < < < | | < < < | | < < < < > | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | ::: === ''ORDERED'', ''UNORDERED'', ''QUOTATION'' These three symbols are specifying different kinds of lists / enumerations: an ordered list, an unordered list, and a quotation list. Their structure is the same. :::syntax __OrderedList__ **=** ''(ORDERED'' [[__Attributes__|00001012931000#attribute]] __ListElement__ … '')''. __UnorderedList__ **=** ''(UNORDERED'' [[__Attributes__|00001012931000#attribute]] __ListElement__ … '')''. __QuotationList__ **=** ''(QUOTATION'' [[__Attributes__|00001012931000#attribute]] __ListElement__ … '')''. ::: :::syntax __ListElement__ **=** [[__Block__|00001012931000#block]] **|** [[__Inline__|00001012931000#inline]]. ::: A list element is either a block or an inline. If it is a block, it may contain a nested list. === ''DESCRIPTION'' :::syntax __Description__ **=** ''(DESCRIPTION'' [[__Attributes__|00001012931000#attribute]] __DescriptionTerm__ __DescriptionValues__ __DescriptionTerm__ __DescriptionValues__ … '')''. ::: A description is a sequence of one or more terms and values. :::syntax __DescriptionTerm__ **=** ''('' [[__InlineElement__|00001012931600]] … '')''. ::: A description term is just an inline-structured value. :::syntax __DescriptionValues__ **=** ''(BLOCK'' [[__Block__|00001012931000#block]] … '')''. ::: Description values are sequences of blocks. === ''TABLE'' :::syntax __Table__ **=** ''(TABLE'' [[__Attributes__|00001012931000#attribute]] __TableHeader__ __TableRow__ … '')''. ::: A table is a table header and a sequence of table rows. :::syntax __TableHeader__ **=** ''()'' **|** ''('' __TableCell__ … '')''. ::: A table header is either the empty list or a list of table cells. :::syntax __TableRow__ **=** ''('' __TableCell__ … '')''. ::: A table row is a list of table cells. === ''CELL'' :::syntax __TableCell__ **=** ''(CELL'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. ::: A table cell contains some attributes and cell content, which is a sequence of inline elements. The attribute ''align'' specifies the cell alignment. An attribute value ''"center"'' specifies centered aligned cell content. The attribute value ''"left"'' specifies left aligned cell content. The attribute value ''"right"'' specifies right aligned cell content. In all other cases, a default alignment is assumed. === ''REGION-*'' The following lists specifies different kinds of regions. A region treat a sequence of block elements to be belonging together in certain ways. The have a similar structure. :::syntax |
︙ | ︙ | |||
163 164 165 166 167 168 169 | :::syntax __ZettelVerbatim__ **=** ''(VERBATIM-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String '')''. ::: The string contains text that should be treated as (nested) zettel content. === ''BLOB'' :::syntax | | | 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | :::syntax __ZettelVerbatim__ **=** ''(VERBATIM-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String '')''. ::: The string contains text that should be treated as (nested) zettel content. === ''BLOB'' :::syntax __BLOB__ **=** ''(BLOB'' [[__Attributes__|00001012931000#attribute]] ''('' [[__InlineElement__|00001012931600]] … '')'' String,,1,, String,,2,, '')''. ::: A BLOB contains an image in block mode. The inline elements states some description. The first string contains the syntax of the image. The second string contains the actual image. If the syntax is ""SVG"", then the second string contains the SVG code. Otherwise the (binary) image data is encoded with base64. |
︙ | ︙ |
Changes to docs/manual/00001012931600.zettel.
1 2 3 4 5 6 | id: 00001012931600 title: Encoding of Sz Inline Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161845 | | | < < < < < < | < < < | < < < < < | < < < < | < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | id: 00001012931600 title: Encoding of Sz Inline Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161845 modified: 20250313130428 === ''TEXT'' :::syntax __Text__ **=** ''(TEXT'' String '')''. ::: Specifies the string as some text content, including white space characters. === ''SOFT'' :::syntax __Soft__ **=** ''(SOFT)''. ::: Denotes a soft line break. It will be often encoded as a space character, but signals the point in the textual content, where a line break occurred. === ''HARD'' :::syntax __Hard__ **=** ''(HARD)''. ::: Specifies a hard line break, i.e. the user wants to have a line break here. === ''LINK'' :::syntax __Link__ **=** ''(LINK'' [[__Attributes__|00001012931000#attribute]] [[__Reference__|00001012931900]] [[__InlineElement__|00001012931600]] … '')''. ::: Specifies a link to some other material. The link type is based on the full reference. The trailing sequence of __InlineElements__ contains the linked text. === ''EMBED'' :::syntax __Embed__ **=** ''(EMBED'' [[__Attributes__|00001012931000#attribute]] [[__Reference__|00001012931900]] String [[__InlineElement__|00001012931600]] … '')''. ::: Specifies an embedded element, in most cases some image content or an inline transclusion. A transclusion will only be used, if the zettel content was not evaluated. |
︙ | ︙ |
Changes to docs/manual/00001012931900.zettel.
1 2 3 4 5 6 | id: 00001012931900 title: Encoding of Sz Reference Values role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230405123046 | | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | id: 00001012931900 title: Encoding of Sz Reference Values role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230405123046 modified: 20250313130616 A reference is encoded as the actual reference value, and a symbol describing the state of that actual reference value. :::syntax __Reference__ **=** ''('' __ReferenceState__ String '')''. ::: The string contains the actual reference value. :::syntax __ReferenceState__ **=** ''INVALID'' **|** ''ZETTEL'' **|** ''SELF'' **|** ''FOUND'' **|** ''BROKEN'' **|** ''HOSTED'' **|** ''BASED'' **|** ''QUERY'' **|** ''EXTERNAL''. ::: ; ''INVALID'' : The reference value is invalid. ; ''ZETTEL'' : The reference value is a reference to a zettel. This value is only possible before evaluating the zettel. ; ''SELF'' : The reference value is a reference to the same zettel, to a specific mark. |
︙ | ︙ |
Deleted encoder/encoder.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/encoder_blob_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/encoder_block_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/encoder_inline_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/encoder_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/htmlenc/htmlenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/mdenc/mdenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/shtmlenc/shtmlenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/szenc/szenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/szenc/transform.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/textenc/textenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/write.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/zmkenc/zmkenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted evaluator/evaluator.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted evaluator/list.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted evaluator/metadata.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to go.mod.
1 2 3 4 5 6 7 8 9 10 11 12 | module zettelstore.de/z go 1.24 require ( github.com/fsnotify/fsnotify v1.8.0 github.com/yuin/goldmark v1.7.8 golang.org/x/crypto v0.36.0 golang.org/x/term v0.30.0 golang.org/x/text v0.23.0 t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3 t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b | > | | > < | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | module zettelstore.de/z go 1.24 require ( github.com/fsnotify/fsnotify v1.8.0 github.com/yuin/goldmark v1.7.8 golang.org/x/crypto v0.36.0 golang.org/x/term v0.30.0 golang.org/x/text v0.23.0 t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3 t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b t73f.de/r/webs v0.0.0-20250311182734-f263a38b32d5 t73f.de/r/zero v0.0.0-20250320133226-a6d535b4985a t73f.de/r/zsc v0.0.0-20250325165515-7fa708d3bd2c t73f.de/r/zsx v0.0.0-20250320152042-d72698d2606a ) require golang.org/x/sys v0.31.0 // indirect |
Changes to go.sum.
︙ | ︙ | |||
10 11 12 13 14 15 16 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3 h1:Jek4x1Qp59SWXI1enWVTeP1wxcVO96FuBpJBnnwOY98= t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3/go.mod h1:hzg05uSCMk3D/DWaL0pdlowfL2aWQeGIfD1S04vV+Xg= t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b h1:X+9mMDd3fKML5SPcQk4n28oDGFUwqjDiSmQrH2LHZwI= t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b/go.mod h1:p+3JCSzNm9e+Yyub0ODRiLDeKaGVYWvBKYANZaAWYIA= | | | | | | | > > | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3 h1:Jek4x1Qp59SWXI1enWVTeP1wxcVO96FuBpJBnnwOY98= t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3/go.mod h1:hzg05uSCMk3D/DWaL0pdlowfL2aWQeGIfD1S04vV+Xg= t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b h1:X+9mMDd3fKML5SPcQk4n28oDGFUwqjDiSmQrH2LHZwI= t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b/go.mod h1:p+3JCSzNm9e+Yyub0ODRiLDeKaGVYWvBKYANZaAWYIA= t73f.de/r/webs v0.0.0-20250311182734-f263a38b32d5 h1:nnKfs/2i9n3S5VjbSj98odcwZKGcL96qPSIUATT/2P8= t73f.de/r/webs v0.0.0-20250311182734-f263a38b32d5/go.mod h1:zk92hSKB4iWyT290+163seNzu350TA9XLATC9kOldqo= t73f.de/r/zero v0.0.0-20250320133226-a6d535b4985a h1:Fph+eBW2QhZ4GX5Pml8zx4vRPy56rOGrBC6GH61UwrQ= t73f.de/r/zero v0.0.0-20250320133226-a6d535b4985a/go.mod h1:T1vFcHoymUQcr7+vENBkS1yryZRZ3YB8uRtnMy8yRBA= t73f.de/r/zsc v0.0.0-20250325165515-7fa708d3bd2c h1:GPDeYmSKnKhUKwSd4okJPRc22Y5ZcVXGTSGx9oaGGTI= t73f.de/r/zsc v0.0.0-20250325165515-7fa708d3bd2c/go.mod h1:ZvrqlLeTKgqg6gttC53hcsE/iAPCDM5+/FXXlRLt+vQ= t73f.de/r/zsx v0.0.0-20250320152042-d72698d2606a h1:+HLfeVMoYfTzeVYnOJYywKmgahE/pqiZrPuNZn53M2k= t73f.de/r/zsx v0.0.0-20250320152042-d72698d2606a/go.mod h1:hZlFM0YBLIMgh2iEdS0rnuxLfO7inpWmc+wyDeRptT8= |
Added internal/ast/ast.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree for parsed zettel content. package ast import ( "net/url" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/zettel" ) // ZettelNode is the root node of the abstract syntax tree. // It is *not* part of the visitor pattern. type ZettelNode struct { Meta *meta.Meta // Original metadata Content zettel.Content // Original content Zid id.Zid // Zettel identification. InhMeta *meta.Meta // Metadata of the zettel, with inherited values. BlocksAST BlockSlice // Zettel abstract syntax tree is a sequence of block nodes. Syntax string // Syntax / parser that produced the Ast } // Node is the interface, all nodes must implement. type Node interface { WalkChildren(v Visitor) } // BlockNode is the interface that all block nodes must implement. type BlockNode interface { Node blockNode() } // ItemNode is a node that can occur as a list item. type ItemNode interface { BlockNode itemNode() } // ItemSlice is a slice of ItemNodes. type ItemSlice []ItemNode // DescriptionNode is a node that contains just textual description. type DescriptionNode interface { ItemNode descriptionNode() } // DescriptionSlice is a slice of DescriptionNodes. type DescriptionSlice []DescriptionNode // InlineNode is the interface that all inline nodes must implement. type InlineNode interface { Node inlineNode() } // Reference is a reference to external or internal material. type Reference struct { URL *url.URL Value string State RefState } // RefState indicates the state of the reference. type RefState int // Constants for RefState const ( RefStateInvalid RefState = iota // Invalid Reference RefStateZettel // Reference to an internal zettel RefStateSelf // Reference to same zettel with a fragment RefStateFound // Reference to an existing internal zettel, URL is ajusted RefStateBroken // Reference to a non-existing internal zettel RefStateHosted // Reference to local hosted non-Zettel, without URL change RefStateBased // Reference to local non-Zettel, to be prefixed RefStateQuery // Reference to a zettel query RefStateExternal // Reference to external material ) // ParseSpacedText returns an inline slice that consists just of test and space node. // No Zettelmarkup parsing is done. It is typically used to transform the zettel // description into an inline slice. func ParseSpacedText(s string) InlineSlice { return InlineSlice{&TextNode{Text: NormalizedSpacedText(s)}} } // NormalizedSpacedText returns the given string, but normalize multiple spaces to one space. func NormalizedSpacedText(s string) string { return strings.Join(strings.Fields(s), " ") } |
Added internal/ast/block.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast import "t73f.de/r/zsx" // Definition of Block nodes. // BlockSlice is a slice of BlockNodes. type BlockSlice []BlockNode func (*BlockSlice) blockNode() { /* Just a marker */ } // WalkChildren walks down to the descriptions. func (bs *BlockSlice) WalkChildren(v Visitor) { if bs != nil { for _, bn := range *bs { Walk(v, bn) } } } // FirstParagraphInlines returns the inline list of the first paragraph that // contains a inline list. func (bs BlockSlice) FirstParagraphInlines() InlineSlice { for _, bn := range bs { pn, ok := bn.(*ParaNode) if !ok { continue } if inl := pn.Inlines; len(inl) > 0 { return inl } } return nil } //-------------------------------------------------------------------------- // ParaNode contains just a sequence of inline elements. // Another name is "paragraph". type ParaNode struct { Inlines InlineSlice } func (*ParaNode) blockNode() { /* Just a marker */ } func (*ParaNode) itemNode() { /* Just a marker */ } func (*ParaNode) descriptionNode() { /* Just a marker */ } // CreateParaNode creates a parameter block from inline nodes. func CreateParaNode(nodes ...InlineNode) *ParaNode { return &ParaNode{Inlines: nodes} } // WalkChildren walks down the inline elements. func (pn *ParaNode) WalkChildren(v Visitor) { Walk(v, &pn.Inlines) } //-------------------------------------------------------------------------- // VerbatimNode contains uninterpreted text type VerbatimNode struct { Kind VerbatimKind Attrs zsx.Attributes Content []byte } // VerbatimKind specifies the format that is applied to code inline nodes. type VerbatimKind int // Constants for VerbatimCode const ( _ VerbatimKind = iota VerbatimZettel // Zettel content VerbatimCode // Program code VerbatimEval // Code to be externally interpreted. Syntax is stored in default attribute. VerbatimComment // Block comment VerbatimHTML // Block HTML, e.g. for Markdown VerbatimMath // Block math mode ) func (*VerbatimNode) blockNode() { /* Just a marker */ } func (*VerbatimNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // RegionNode encapsulates a region of block nodes. type RegionNode struct { Kind RegionKind Attrs zsx.Attributes Blocks BlockSlice Inlines InlineSlice // Optional text at the end of the region } // RegionKind specifies the actual region type. type RegionKind int // Values for RegionCode const ( _ RegionKind = iota RegionSpan // Just a span of blocks RegionQuote // A longer quotation RegionVerse // Line breaks matter ) func (*RegionNode) blockNode() { /* Just a marker */ } func (*RegionNode) itemNode() { /* Just a marker */ } // WalkChildren walks down the blocks and the text. func (rn *RegionNode) WalkChildren(v Visitor) { Walk(v, &rn.Blocks) Walk(v, &rn.Inlines) } //-------------------------------------------------------------------------- // HeadingNode stores the heading text and level. type HeadingNode struct { Level int Attrs zsx.Attributes Slug string // Heading text, normalized Fragment string // Heading text, suitable to be used as an unique URL fragment Inlines InlineSlice // Heading text, possibly formatted } func (*HeadingNode) blockNode() { /* Just a marker */ } func (*HeadingNode) itemNode() { /* Just a marker */ } // WalkChildren walks the heading text. func (hn *HeadingNode) WalkChildren(v Visitor) { Walk(v, &hn.Inlines) } //-------------------------------------------------------------------------- // HRuleNode specifies a horizontal rule. type HRuleNode struct { Attrs zsx.Attributes } func (*HRuleNode) blockNode() { /* Just a marker */ } func (*HRuleNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (*HRuleNode) WalkChildren(Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // NestedListNode specifies a nestable list, either ordered or unordered. type NestedListNode struct { Kind NestedListKind Attrs zsx.Attributes Items []ItemSlice } // NestedListKind specifies the actual list type. type NestedListKind uint8 // Values for ListCode const ( _ NestedListKind = iota NestedListOrdered // Ordered list. NestedListUnordered // Unordered list. NestedListQuote // Quote list. ) func (*NestedListNode) blockNode() { /* Just a marker */ } func (*NestedListNode) itemNode() { /* Just a marker */ } // WalkChildren walks down the items. func (ln *NestedListNode) WalkChildren(v Visitor) { if items := ln.Items; items != nil { for _, item := range items { WalkItemSlice(v, item) } } } //-------------------------------------------------------------------------- // DescriptionListNode specifies a description list. type DescriptionListNode struct { Attrs zsx.Attributes Descriptions []Description } // Description is one element of a description list. type Description struct { Term InlineSlice Descriptions []DescriptionSlice } func (*DescriptionListNode) blockNode() { /* Just a marker */ } // WalkChildren walks down to the descriptions. func (dn *DescriptionListNode) WalkChildren(v Visitor) { if descrs := dn.Descriptions; descrs != nil { for i, desc := range descrs { if len(desc.Term) > 0 { Walk(v, &descrs[i].Term) // Otherwise, changes in desc.Term will not go back into AST } if dss := desc.Descriptions; dss != nil { for _, dns := range dss { WalkDescriptionSlice(v, dns) } } } } } //-------------------------------------------------------------------------- // TableNode specifies a full table type TableNode struct { Header TableRow // The header row Align []Alignment // Default column alignment Rows []TableRow // The slice of cell rows } // TableCell contains the data for one table cell type TableCell struct { Align Alignment // Cell alignment Inlines InlineSlice // Cell content } // TableRow is a slice of cells. type TableRow []*TableCell // Alignment specifies text alignment. // Currently only for tables. type Alignment int // Constants for Alignment. const ( _ Alignment = iota AlignDefault // Default alignment, inherited AlignLeft // Left alignment AlignCenter // Center the content AlignRight // Right alignment ) func (*TableNode) blockNode() { /* Just a marker */ } // WalkChildren walks down to the cells. func (tn *TableNode) WalkChildren(v Visitor) { if header := tn.Header; header != nil { for i := range header { Walk(v, header[i]) // Otherwise changes will not go back } } if rows := tn.Rows; rows != nil { for _, row := range rows { for i := range row { Walk(v, &row[i].Inlines) // Otherwise changes will not go back } } } } // WalkChildren walks the list of inline elements. func (cell *TableCell) WalkChildren(v Visitor) { Walk(v, &cell.Inlines) // Otherwise changes will not go back } //-------------------------------------------------------------------------- // TranscludeNode specifies block content from other zettel to embedded in // current zettel type TranscludeNode struct { Attrs zsx.Attributes Ref *Reference Inlines InlineSlice // Optional text. } func (*TranscludeNode) blockNode() { /* Just a marker */ } // WalkChildren walks the associated text. func (tn *TranscludeNode) WalkChildren(v Visitor) { Walk(v, &tn.Inlines) } //-------------------------------------------------------------------------- // BLOBNode contains just binary data that must be interpreted according to // a syntax. type BLOBNode struct { Attrs zsx.Attributes Description InlineSlice Syntax string Blob []byte } func (*BLOBNode) blockNode() { /* Just a marker */ } // WalkChildren does nothing. func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ } |
Added internal/ast/inline.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast import "t73f.de/r/zsx" // Definitions of inline nodes. // InlineSlice is a list of BlockNodes. type InlineSlice []InlineNode func (*InlineSlice) inlineNode() { /* Just a marker */ } // WalkChildren walks down to the list. func (is *InlineSlice) WalkChildren(v Visitor) { if is != nil { for _, in := range *is { Walk(v, in) } } } // -------------------------------------------------------------------------- // TextNode just contains some text. type TextNode struct { Text string // The text itself. } func (*TextNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (*TextNode) WalkChildren(Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // BreakNode signals a new line that must / should be interpreted as a new line break. type BreakNode struct { Hard bool // Hard line break? } func (*BreakNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (*BreakNode) WalkChildren(Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // LinkNode contains the specified link. type LinkNode struct { Attrs zsx.Attributes // Optional attributes Ref *Reference Inlines InlineSlice // The text associated with the link. } func (*LinkNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the link text. func (ln *LinkNode) WalkChildren(v Visitor) { if len(ln.Inlines) > 0 { Walk(v, &ln.Inlines) } } // -------------------------------------------------------------------------- // EmbedRefNode contains the specified embedded reference material. type EmbedRefNode struct { Attrs zsx.Attributes // Optional attributes Ref *Reference // The reference to be embedded. Syntax string // Syntax of referenced material, if known Inlines InlineSlice // Optional text associated with the image. } func (*EmbedRefNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the text that describes the embedded material. func (en *EmbedRefNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) } // -------------------------------------------------------------------------- // EmbedBLOBNode contains the specified embedded BLOB material. type EmbedBLOBNode struct { Attrs zsx.Attributes // Optional attributes Syntax string // Syntax of Blob Blob []byte // BLOB data itself. Inlines InlineSlice // Optional text associated with the image. } func (*EmbedBLOBNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the text that describes the embedded material. func (en *EmbedBLOBNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) } // -------------------------------------------------------------------------- // CiteNode contains the specified citation. type CiteNode struct { Attrs zsx.Attributes // Optional attributes Key string // The citation key Inlines InlineSlice // Optional text associated with the citation. } func (*CiteNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the cite text. func (cn *CiteNode) WalkChildren(v Visitor) { Walk(v, &cn.Inlines) } // -------------------------------------------------------------------------- // MarkNode contains the specified merked position. // It is a BlockNode too, because although it is typically parsed during inline // mode, it is moved into block mode afterwards. type MarkNode struct { Mark string // The mark text itself Slug string // Slugified form of Mark Fragment string // Unique form of Slug Inlines InlineSlice // Marked inline content } func (*MarkNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (mn *MarkNode) WalkChildren(v Visitor) { if len(mn.Inlines) > 0 { Walk(v, &mn.Inlines) } } // -------------------------------------------------------------------------- // FootnoteNode contains the specified footnote. type FootnoteNode struct { Attrs zsx.Attributes // Optional attributes Inlines InlineSlice // The footnote text. } func (*FootnoteNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the footnote text. func (fn *FootnoteNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) } // -------------------------------------------------------------------------- // FormatNode specifies some inline formatting. type FormatNode struct { Kind FormatKind Attrs zsx.Attributes // Optional attributes. Inlines InlineSlice } // FormatKind specifies the format that is applied to the inline nodes. type FormatKind int // Constants for FormatCode const ( _ FormatKind = iota FormatEmph // Emphasized text FormatStrong // Strongly emphasized text FormatInsert // Inserted text FormatDelete // Deleted text FormatSuper // Superscripted text FormatSub // SubscriptedText FormatQuote // Quoted text FormatMark // Marked text FormatSpan // Generic inline container ) func (*FormatNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the formatted text. func (fn *FormatNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) } // -------------------------------------------------------------------------- // LiteralNode specifies some uninterpreted text. type LiteralNode struct { Kind LiteralKind Attrs zsx.Attributes // Optional attributes. Content []byte } // LiteralKind specifies the format that is applied to code inline nodes. type LiteralKind int // Constants for LiteralCode const ( _ LiteralKind = iota LiteralCode // Inline program code LiteralInput // Computer input, e.g. Keyboard strokes LiteralOutput // Computer output LiteralComment // Inline comment LiteralMath // Inline math mode ) func (*LiteralNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (*LiteralNode) WalkChildren(Visitor) { /* No children*/ } |
Added internal/ast/ref.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast import ( "net/url" "strings" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" ) // QueryPrefix is the prefix that denotes a query expression. const QueryPrefix = api.QueryPrefix // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { if invalidReference(s) { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } if strings.HasPrefix(s, QueryPrefix) { return &Reference{URL: nil, Value: s[len(QueryPrefix):], State: RefStateQuery} } if state, ok := localState(s); ok { if state == RefStateBased { s = s[1:] } u, err := url.Parse(s) if err == nil { return &Reference{URL: u, Value: s, State: state} } } u, err := url.Parse(s) if err != nil { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } if !externalURL(u) { if _, err = id.Parse(u.Path); err == nil { return &Reference{URL: u, Value: s, State: RefStateZettel} } if u.Path == "" && u.Fragment != "" { return &Reference{URL: u, Value: s, State: RefStateSelf} } } return &Reference{URL: u, Value: s, State: RefStateExternal} } func invalidReference(s string) bool { return s == "" || s == "00000000000000" } func externalURL(u *url.URL) bool { return u.Scheme != "" || u.Opaque != "" || u.Host != "" || u.User != nil } func localState(path string) (RefState, bool) { if len(path) > 0 && path[0] == '/' { if len(path) > 1 && path[1] == '/' { return RefStateBased, true } return RefStateHosted, true } if len(path) > 1 && path[0] == '.' { if len(path) > 2 && path[1] == '.' && path[2] == '/' { return RefStateHosted, true } return RefStateHosted, path[1] == '/' } return RefStateInvalid, false } // String returns the string representation of a reference. func (r Reference) String() string { if r.State == RefStateQuery { return QueryPrefix + r.Value } if r.URL != nil { return r.URL.String() } return r.Value } // IsValid returns true if reference is valid func (r *Reference) IsValid() bool { return r.State != RefStateInvalid } // IsZettel returns true if it is a referencen to a local zettel. func (r *Reference) IsZettel() bool { switch r.State { case RefStateZettel, RefStateSelf, RefStateFound, RefStateBroken: return true } return false } // IsLocal returns true if reference is local func (r *Reference) IsLocal() bool { return r.State == RefStateHosted || r.State == RefStateBased } // IsExternal returns true if it is a referencen to external material. func (r *Reference) IsExternal() bool { return r.State == RefStateExternal } |
Added internal/ast/ref_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast_test import ( "testing" "zettelstore.de/z/internal/ast" ) func TestParseReference(t *testing.T) { t.Parallel() testcases := []struct { link string err bool exp string }{ {"", true, ""}, {"123", false, "123"}, {",://", true, ""}, } for i, tc := range testcases { got := ast.ParseReference(tc.link) if got.IsValid() == tc.err { t.Errorf( "TC=%d, expected parse error of %q: %v, but got %q", i, tc.link, tc.err, got) } if got.IsValid() && got.String() != tc.exp { t.Errorf("TC=%d, Reference of %q is %q, but got %q", i, tc.link, tc.exp, got) } } } func TestReferenceIsZettelMaterial(t *testing.T) { t.Parallel() testcases := []struct { link string isZettel bool isExternal bool isLocal bool }{ {"", false, false, false}, {"00000000000000", false, false, false}, {"http://zettelstore.de/z/ast", false, true, false}, {"12345678901234", true, false, false}, {"12345678901234#local", true, false, false}, {"http://12345678901234", false, true, false}, {"http://zettelstore.de/z/12345678901234", false, true, false}, {"http://zettelstore.de/12345678901234", false, true, false}, {"/12345678901234", false, false, true}, {"//12345678901234", false, false, true}, {"./12345678901234", false, false, true}, {"../12345678901234", false, false, true}, {".../12345678901234", false, true, false}, } for i, tc := range testcases { ref := ast.ParseReference(tc.link) isZettel := ref.IsZettel() if isZettel != tc.isZettel { t.Errorf( "TC=%d, Reference %q isZettel=%v expected, but got %v", i, tc.link, tc.isZettel, isZettel) } isLocal := ref.IsLocal() if isLocal != tc.isLocal { t.Errorf( "TC=%d, Reference %q isLocal=%v expected, but got %v", i, tc.link, tc.isLocal, isLocal) } isExternal := ref.IsExternal() if isExternal != tc.isExternal { t.Errorf( "TC=%d, Reference %q isExternal=%v expected, but got %v", i, tc.link, tc.isExternal, isExternal) } } } |
Added internal/ast/sztrans/sztrans.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 | //----------------------------------------------------------------------------- // Copyright (c) 2025-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2025-present Detlef Stern //----------------------------------------------------------------------------- // Package sztrans allows to transform a sz representation of text into an // abstract syntax tree. package sztrans import ( "fmt" "log" "t73f.de/r/sx" "t73f.de/r/zsc/sz" "t73f.de/r/zsx" "zettelstore.de/z/internal/ast" ) type transformer struct{} // GetBlockSlice returns the sz representations as a AST BlockSlice func GetBlockSlice(pair *sx.Pair) (ast.BlockSlice, error) { if pair == nil { return nil, nil } var t transformer if obj := zsx.Walk(&t, pair, nil); !obj.IsNil() { if sxn, isNode := obj.(sxNode); isNode { if bs, ok := sxn.node.(*ast.BlockSlice); ok { return *bs, nil } return nil, fmt.Errorf("no BlockSlice AST: %T/%v for %v", sxn.node, sxn.node, pair) } return nil, fmt.Errorf("no AST for %v: %v", pair, obj) } return nil, fmt.Errorf("error walking %v", pair) } func (t *transformer) VisitBefore(pair *sx.Pair, _ *sx.Pair) (sx.Object, bool) { if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol { switch sym { case zsx.SymText: if p := pair.Tail(); p != nil { if s, isString := sx.GetString(p.Car()); isString { return sxNode{&ast.TextNode{Text: s.GetValue()}}, true } } case zsx.SymSoft: return sxNode{&ast.BreakNode{Hard: false}}, true case zsx.SymHard: return sxNode{&ast.BreakNode{Hard: true}}, true case zsx.SymLiteralCode: return handleLiteral(ast.LiteralCode, pair.Tail()) case zsx.SymLiteralComment: return handleLiteral(ast.LiteralComment, pair.Tail()) case zsx.SymLiteralInput: return handleLiteral(ast.LiteralInput, pair.Tail()) case zsx.SymLiteralMath: return handleLiteral(ast.LiteralMath, pair.Tail()) case zsx.SymLiteralOutput: return handleLiteral(ast.LiteralOutput, pair.Tail()) case zsx.SymThematic: return sxNode{&ast.HRuleNode{Attrs: zsx.GetAttributes(pair.Tail().Head())}}, true case zsx.SymVerbatimComment: return handleVerbatim(ast.VerbatimComment, pair.Tail()) case zsx.SymVerbatimEval: return handleVerbatim(ast.VerbatimEval, pair.Tail()) case zsx.SymVerbatimHTML: return handleVerbatim(ast.VerbatimHTML, pair.Tail()) case zsx.SymVerbatimMath: return handleVerbatim(ast.VerbatimMath, pair.Tail()) case zsx.SymVerbatimCode: return handleVerbatim(ast.VerbatimCode, pair.Tail()) case zsx.SymVerbatimZettel: return handleVerbatim(ast.VerbatimZettel, pair.Tail()) } } return sx.Nil(), false } func handleLiteral(kind ast.LiteralKind, rest *sx.Pair) (sx.Object, bool) { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if s, isString := sx.GetString(curr.Car()); isString { return sxNode{&ast.LiteralNode{ Kind: kind, Attrs: attrs, Content: []byte(s.GetValue())}}, true } } } return nil, false } func handleVerbatim(kind ast.VerbatimKind, rest *sx.Pair) (sx.Object, bool) { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if s, isString := sx.GetString(curr.Car()); isString { return sxNode{&ast.VerbatimNode{ Kind: kind, Attrs: attrs, Content: []byte(s.GetValue()), }}, true } } } return nil, false } func (t *transformer) VisitAfter(pair *sx.Pair, _ *sx.Pair) sx.Object { if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol { switch sym { case zsx.SymBlock: bns := collectBlocks(pair.Tail()) return sxNode{&bns} case zsx.SymPara: return sxNode{&ast.ParaNode{Inlines: collectInlines(pair.Tail())}} case zsx.SymHeading: return handleHeading(pair.Tail()) case zsx.SymListOrdered: return handleList(ast.NestedListOrdered, pair.Tail()) case zsx.SymListUnordered: return handleList(ast.NestedListUnordered, pair.Tail()) case zsx.SymListQuote: return handleList(ast.NestedListQuote, pair.Tail()) case zsx.SymDescription: return handleDescription(pair.Tail()) case zsx.SymTable: return handleTable(pair.Tail()) case zsx.SymCell: return handleCell(pair.Tail()) case zsx.SymRegionBlock: return handleRegion(ast.RegionSpan, pair.Tail()) case zsx.SymRegionQuote: return handleRegion(ast.RegionQuote, pair.Tail()) case zsx.SymRegionVerse: return handleRegion(ast.RegionVerse, pair.Tail()) case zsx.SymTransclude: return handleTransclude(pair.Tail()) case zsx.SymBLOB: return handleBLOB(pair.Tail()) case zsx.SymLink: return handleLink(pair.Tail()) case zsx.SymEmbed: return handleEmbed(pair.Tail()) case zsx.SymEmbedBLOB: return handleEmbedBLOB(pair.Tail()) case zsx.SymCite: return handleCite(pair.Tail()) case zsx.SymMark: return handleMark(pair.Tail()) case zsx.SymEndnote: return handleEndnote(pair.Tail()) case zsx.SymFormatDelete: return handleFormat(ast.FormatDelete, pair.Tail()) case zsx.SymFormatEmph: return handleFormat(ast.FormatEmph, pair.Tail()) case zsx.SymFormatInsert: return handleFormat(ast.FormatInsert, pair.Tail()) case zsx.SymFormatMark: return handleFormat(ast.FormatMark, pair.Tail()) case zsx.SymFormatQuote: return handleFormat(ast.FormatQuote, pair.Tail()) case zsx.SymFormatSpan: return handleFormat(ast.FormatSpan, pair.Tail()) case zsx.SymFormatSub: return handleFormat(ast.FormatSub, pair.Tail()) case zsx.SymFormatSuper: return handleFormat(ast.FormatSuper, pair.Tail()) case zsx.SymFormatStrong: return handleFormat(ast.FormatStrong, pair.Tail()) } log.Println("MISS", pair) } return pair } func collectBlocks(lst *sx.Pair) (result ast.BlockSlice) { for val := range lst.Values() { if sxn, isNode := val.(sxNode); isNode { if bn, isInline := sxn.node.(ast.BlockNode); isInline { result = append(result, bn) } } } return result } func collectInlines(lst *sx.Pair) (result ast.InlineSlice) { for val := range lst.Values() { if sxn, isNode := val.(sxNode); isNode { if in, isInline := sxn.node.(ast.InlineNode); isInline { result = append(result, in) } } } return result } func handleHeading(rest *sx.Pair) sx.Object { if rest != nil { if num, isNumber := rest.Car().(sx.Int64); isNumber && num > 0 && num < 6 { if curr := rest.Tail(); curr != nil { attrs := zsx.GetAttributes(curr.Head()) if curr = curr.Tail(); curr != nil { if sSlug, isSlug := sx.GetString(curr.Car()); isSlug { if curr = curr.Tail(); curr != nil { if sUniq, isUniq := sx.GetString(curr.Car()); isUniq { return sxNode{&ast.HeadingNode{ Level: int(num), Attrs: attrs, Slug: sSlug.GetValue(), Fragment: sUniq.GetValue(), Inlines: collectInlines(curr.Tail()), }} } } } } } } } log.Println("HEAD", rest) return rest } func handleList(kind ast.NestedListKind, rest *sx.Pair) sx.Object { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) return sxNode{&ast.NestedListNode{ Kind: kind, Items: collectItemSlices(rest.Tail()), Attrs: attrs}} } log.Println("LIST", kind, rest) return rest } func collectItemSlices(lst *sx.Pair) (result []ast.ItemSlice) { for val := range lst.Values() { if sxn, isNode := val.(sxNode); isNode { if bns, isBlockSlice := sxn.node.(*ast.BlockSlice); isBlockSlice { items := make(ast.ItemSlice, len(*bns)) for i, bn := range *bns { if it, ok := bn.(ast.ItemNode); ok { items[i] = it } } result = append(result, items) } if ins, isInline := sxn.node.(*ast.InlineSlice); isInline { items := make(ast.ItemSlice, len(*ins)) for i, bn := range *ins { if it, ok := bn.(ast.ItemNode); ok { items[i] = it } } result = append(result, items) } } } return result } func handleDescription(rest *sx.Pair) sx.Object { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) var descs []ast.Description for curr := rest.Tail(); curr != nil; { term := collectInlines(curr.Head()) curr = curr.Tail() if curr == nil { descr := ast.Description{Term: term, Descriptions: nil} descs = append(descs, descr) break } car := curr.Car() if sx.IsNil(car) { descs = append(descs, ast.Description{Term: term, Descriptions: nil}) curr = curr.Tail() continue } sxn, isNode := car.(sxNode) if !isNode { descs = nil break } blocks, isBlocks := sxn.node.(*ast.BlockSlice) if !isBlocks { descs = nil break } descSlice := make([]ast.DescriptionSlice, 0, len(*blocks)) for _, bn := range *blocks { bns, isBns := bn.(*ast.BlockSlice) if !isBns { continue } ds := make(ast.DescriptionSlice, 0, len(*bns)) for _, b := range *bns { if defNode, isDef := b.(ast.DescriptionNode); isDef { ds = append(ds, defNode) } } descSlice = append(descSlice, ds) } descr := ast.Description{Term: term, Descriptions: descSlice} descs = append(descs, descr) curr = curr.Tail() } if len(descs) > 0 { return sxNode{&ast.DescriptionListNode{ Attrs: attrs, Descriptions: descs, }} } } log.Println("DESC", rest) return rest } func handleTable(rest *sx.Pair) sx.Object { if rest != nil { header := collectRow(rest.Head()) cols := len(header) var rows []ast.TableRow for curr := range rest.Tail().Pairs() { row := collectRow(curr.Head()) rows = append(rows, row) cols = max(cols, len(row)) } align := make([]ast.Alignment, cols) for i := range cols { align[i] = ast.AlignDefault } return sxNode{&ast.TableNode{ Header: header, Align: align, Rows: rows, }} } log.Println("TABL", rest) return rest } func collectRow(lst *sx.Pair) (row ast.TableRow) { for curr := range lst.Values() { if sxn, isNode := curr.(sxNode); isNode { if cell, isCell := sxn.node.(*ast.TableCell); isCell { row = append(row, cell) } } } return row } func handleCell(rest *sx.Pair) sx.Object { if rest != nil { align := ast.AlignDefault if alignPair := rest.Head().Assoc(zsx.SymAttrAlign); alignPair != nil { if alignValue := alignPair.Cdr(); zsx.AttrAlignCenter.IsEqual(alignValue) { align = ast.AlignCenter } else if zsx.AttrAlignLeft.IsEqual(alignValue) { align = ast.AlignLeft } else if zsx.AttrAlignRight.IsEqual(alignValue) { align = ast.AlignRight } } return sxNode{&ast.TableCell{ Align: align, Inlines: collectInlines(rest.Tail()), }} } log.Println("CELL", rest) return rest } func handleRegion(kind ast.RegionKind, rest *sx.Pair) sx.Object { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if blockList := curr.Head(); blockList != nil { return sxNode{&ast.RegionNode{ Kind: kind, Attrs: attrs, Blocks: collectBlocks(blockList), Inlines: collectInlines(curr.Tail()), }} } } } log.Println("REGI", rest) return rest } func handleTransclude(rest *sx.Pair) sx.Object { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { ref := collectReference(curr.Head()) return sxNode{&ast.TranscludeNode{ Attrs: attrs, Ref: ref, Inlines: collectInlines(curr.Tail()), }} } } log.Println("TRAN", rest) return rest } func handleBLOB(rest *sx.Pair) sx.Object { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { ins := collectInlines(curr.Head()) if curr = curr.Tail(); curr != nil { if syntax, isString := sx.GetString(curr.Car()); isString { if curr = curr.Tail(); curr != nil { if blob, isBlob := sx.GetString(curr.Car()); isBlob { return sxNode{&ast.BLOBNode{ Attrs: attrs, Description: ins, Syntax: syntax.GetValue(), Blob: []byte(blob.GetValue()), }} } } } } } } log.Println("BLOB", rest) return rest } var mapRefState = map[*sx.Symbol]ast.RefState{ zsx.SymRefStateInvalid: ast.RefStateInvalid, sz.SymRefStateZettel: ast.RefStateZettel, zsx.SymRefStateSelf: ast.RefStateSelf, sz.SymRefStateFound: ast.RefStateFound, sz.SymRefStateBroken: ast.RefStateBroken, zsx.SymRefStateHosted: ast.RefStateHosted, sz.SymRefStateBased: ast.RefStateBased, sz.SymRefStateQuery: ast.RefStateQuery, zsx.SymRefStateExternal: ast.RefStateExternal, } func handleLink(rest *sx.Pair) sx.Object { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if szref := curr.Head(); szref != nil { if stateSym, isSym := sx.GetSymbol(szref.Car()); isSym { refval, isString := sx.GetString(szref.Cdr()) if !isString { refval, isString = sx.GetString(szref.Tail().Car()) } if isString { ref := ast.ParseReference(refval.GetValue()) ref.State = mapRefState[stateSym] ins := collectInlines(curr.Tail()) return sxNode{&ast.LinkNode{ Attrs: attrs, Ref: ref, Inlines: ins, }} } } } } } log.Println("LINK", rest) return rest } func handleEmbed(rest *sx.Pair) sx.Object { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if ref := collectReference(curr.Head()); ref != nil { if curr = curr.Tail(); curr != nil { if syntax, isString := sx.GetString(curr.Car()); isString { return sxNode{&ast.EmbedRefNode{ Attrs: attrs, Ref: ref, Syntax: syntax.GetValue(), Inlines: collectInlines(curr.Tail()), }} } } } } } log.Println("EMBE", rest) return rest } func handleEmbedBLOB(rest *sx.Pair) sx.Object { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if syntax, isSyntax := sx.GetString(curr.Car()); isSyntax { if curr = curr.Tail(); curr != nil { if content, isContent := sx.GetString(curr.Car()); isContent { return sxNode{&ast.EmbedBLOBNode{ Attrs: attrs, Syntax: syntax.GetValue(), Blob: []byte(content.GetValue()), Inlines: collectInlines(curr.Tail()), }} } } } } } log.Println("EMBL", rest) return rest } func collectReference(pair *sx.Pair) *ast.Reference { if pair != nil { if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol { if next := pair.Tail(); next != nil { if sRef, isString := sx.GetString(next.Car()); isString { ref := ast.ParseReference(sRef.GetValue()) switch sym { case zsx.SymRefStateInvalid: ref.State = ast.RefStateInvalid case sz.SymRefStateZettel: ref.State = ast.RefStateZettel case zsx.SymRefStateSelf: ref.State = ast.RefStateSelf case sz.SymRefStateFound: ref.State = ast.RefStateFound case sz.SymRefStateBroken: ref.State = ast.RefStateBroken case zsx.SymRefStateHosted: ref.State = ast.RefStateHosted case sz.SymRefStateBased: ref.State = ast.RefStateBased case sz.SymRefStateQuery: ref.State = ast.RefStateQuery case zsx.SymRefStateExternal: ref.State = ast.RefStateExternal } return ref } } } } return nil } func handleCite(rest *sx.Pair) sx.Object { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if sKey, isString := sx.GetString(curr.Car()); isString { return sxNode{&ast.CiteNode{ Attrs: attrs, Key: sKey.GetValue(), Inlines: collectInlines(curr.Tail()), }} } } } log.Println("CITE", rest) return rest } func handleMark(rest *sx.Pair) sx.Object { if rest != nil { if sMark, isMarkS := sx.GetString(rest.Car()); isMarkS { if curr := rest.Tail(); curr != nil { if sSlug, isSlug := sx.GetString(curr.Car()); isSlug { if curr = curr.Tail(); curr != nil { if sUniq, isUniq := sx.GetString(curr.Car()); isUniq { return sxNode{&ast.MarkNode{ Mark: sMark.GetValue(), Slug: sSlug.GetValue(), Fragment: sUniq.GetValue(), Inlines: collectInlines(curr.Tail()), }} } } } } } } log.Println("MARK", rest) return rest } func handleEndnote(rest *sx.Pair) sx.Object { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) return sxNode{&ast.FootnoteNode{ Attrs: attrs, Inlines: collectInlines(rest.Tail()), }} } log.Println("ENDN", rest) return rest } func handleFormat(kind ast.FormatKind, rest *sx.Pair) sx.Object { if rest != nil { attrs := zsx.GetAttributes(rest.Head()) return sxNode{&ast.FormatNode{ Kind: kind, Attrs: attrs, Inlines: collectInlines(rest.Tail()), }} } log.Println("FORM", kind, rest) return rest } type sxNode struct { node ast.Node } func (sxNode) IsNil() bool { return false } func (sxNode) IsAtom() bool { return true } func (n sxNode) String() string { return fmt.Sprintf("%T/%v", n.node, n.node) } func (n sxNode) GoString() string { return n.String() } func (n sxNode) IsEqual(other sx.Object) bool { return n.String() == other.String() } |
Added internal/ast/walk.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package ast // Visitor is a visitor for walking the AST. type Visitor interface { Visit(node Node) Visitor } // Walk traverses the AST. func Walk(v Visitor, node Node) { if v = v.Visit(node); v == nil { return } // Implementation note: // It is much faster to use interface dispatching than to use a switch statement. // On my "cpu: Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz", a switch statement // implementation tooks approx 940-980 ns/op. Interface dispatching is in the // range of 900-930 ns/op. node.WalkChildren(v) v.Visit(nil) } // WalkItemSlice traverses an item slice. func WalkItemSlice(v Visitor, ins ItemSlice) { for _, in := range ins { Walk(v, in) } } // WalkDescriptionSlice traverses an item slice. func WalkDescriptionSlice(v Visitor, dns DescriptionSlice) { for _, dn := range dns { Walk(v, dn) } } |
Added internal/ast/walk_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package ast_test import ( "testing" "t73f.de/r/zsx" "zettelstore.de/z/internal/ast" ) func BenchmarkWalk(b *testing.B) { root := ast.BlockSlice{ &ast.HeadingNode{ Inlines: ast.InlineSlice{&ast.TextNode{Text: "A Simple Heading"}}, }, &ast.ParaNode{ Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is the introduction."}}, }, &ast.NestedListNode{ Kind: ast.NestedListUnordered, Items: []ast.ItemSlice{ []ast.ItemNode{ &ast.ParaNode{ Inlines: ast.InlineSlice{&ast.TextNode{Text: "Item 1"}}, }, }, []ast.ItemNode{ &ast.ParaNode{ Inlines: ast.InlineSlice{&ast.TextNode{Text: "Item 2"}}, }, }, }, }, &ast.ParaNode{ Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is some intermediate text."}}, }, ast.CreateParaNode( &ast.FormatNode{ Kind: ast.FormatEmph, Attrs: zsx.Attributes(map[string]string{ "": "class", "color": "green", }), Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is some emphasized text."}}, }, &ast.TextNode{Text: " "}, &ast.LinkNode{ Ref: &ast.Reference{Value: "http://zettelstore.de"}, Inlines: ast.InlineSlice{&ast.TextNode{Text: "URL text."}}, }, ), } v := benchVisitor{} for b.Loop() { ast.Walk(&v, &root) } } type benchVisitor struct{} func (bv *benchVisitor) Visit(ast.Node) ast.Visitor { return bv } |
Added internal/auth/auth.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package auth provides services for authentification / authorization. package auth import ( "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" ) // BaseManager allows to check some base auth modes. type BaseManager interface { // IsReadonly returns true, if the systems is configured to run in read-only-mode. IsReadonly() bool } // TokenManager provides methods to create authentication type TokenManager interface { // GetToken produces a authentication token. GetToken(ident *meta.Meta, d time.Duration, kind TokenKind) ([]byte, error) // CheckToken checks the validity of the token and returns relevant data. CheckToken(token []byte, k TokenKind) (TokenData, error) } // TokenKind specifies for which application / usage a token is/was requested. type TokenKind int // Allowed values of token kind const ( _ TokenKind = iota KindAPI KindwebUI ) // TokenData contains some important elements from a token. type TokenData struct { Token []byte Now time.Time Issued time.Time Expires time.Time Ident string Zid id.Zid } // AuthzManager provides methods for authorization. type AuthzManager interface { BaseManager // Owner returns the zettel identifier of the owner. Owner() id.Zid // IsOwner returns true, if the given zettel identifier is that of the owner. IsOwner(zid id.Zid) bool // Returns true if authentication is enabled. WithAuth() bool // GetUserRole role returns the user role of the given user zettel. GetUserRole(user *meta.Meta) meta.UserRole } // Manager is the main interface for providing the service. type Manager interface { TokenManager AuthzManager BoxWithPolicy(unprotectedBox box.Box, rtConfig config.Config) (box.Box, Policy) } // Policy is an interface for checking access authorization. type Policy interface { // User is allowed to create a new zettel. CanCreate(user, newMeta *meta.Meta) bool // User is allowed to read zettel CanRead(user, m *meta.Meta) bool // User is allowed to write zettel. CanWrite(user, oldMeta, newMeta *meta.Meta) bool // User is allowed to delete zettel. CanDelete(user, m *meta.Meta) bool // User is allowed to refresh box data. CanRefresh(user *meta.Meta) bool } |
Added internal/auth/cred/cred.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package cred provides some function for handling credentials. package cred import ( "bytes" "golang.org/x/crypto/bcrypt" "t73f.de/r/zsc/domain/id" ) // HashCredential returns a hashed vesion of the given credential func HashCredential(zid id.Zid, ident, credential string) (string, error) { fullCredential := createFullCredential(zid, ident, credential) res, err := bcrypt.GenerateFromPassword(fullCredential, bcrypt.DefaultCost) if err != nil { return "", err } return string(res), nil } // CompareHashAndCredential checks, whether the hashed credential is a possible // value when hashing the credential. func CompareHashAndCredential(hashed string, zid id.Zid, ident, credential string) (bool, error) { fullCredential := createFullCredential(zid, ident, credential) err := bcrypt.CompareHashAndPassword([]byte(hashed), fullCredential) if err == nil { return true, nil } if err == bcrypt.ErrMismatchedHashAndPassword { return false, nil } return false, err } func createFullCredential(zid id.Zid, ident, credential string) []byte { var buf bytes.Buffer buf.Write(zid.Bytes()) buf.WriteByte(' ') buf.WriteString(ident) buf.WriteByte(' ') buf.WriteString(credential) return buf.Bytes() } |
Added internal/auth/impl/digest.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | //----------------------------------------------------------------------------- // Copyright (c) 2023-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( "bytes" "crypto" "crypto/hmac" "encoding/base64" "t73f.de/r/sx" "t73f.de/r/sx/sxreader" ) var encoding = base64.RawURLEncoding const digestAlg = crypto.SHA384 func sign(claim sx.Object, secret []byte) ([]byte, error) { var buf bytes.Buffer _, err := sx.Print(&buf, claim) if err != nil { return nil, err } token := make([]byte, encoding.EncodedLen(buf.Len())) encoding.Encode(token, buf.Bytes()) digest := hmac.New(digestAlg.New, secret) _, err = digest.Write(buf.Bytes()) if err != nil { return nil, err } dig := digest.Sum(nil) encDig := make([]byte, encoding.EncodedLen(len(dig))) encoding.Encode(encDig, dig) token = append(token, '.') token = append(token, encDig...) return token, nil } func check(token []byte, secret []byte) (sx.Object, error) { i := bytes.IndexByte(token, '.') if i <= 0 || 1024 < i { return nil, ErrMalformedToken } buf := make([]byte, len(token)) n, err := encoding.Decode(buf, token[:i]) if err != nil { return nil, err } rdr := sxreader.MakeReader(bytes.NewReader(buf[:n])) obj, err := rdr.Read() if err != nil { return nil, err } var objBuf bytes.Buffer _, err = sx.Print(&objBuf, obj) if err != nil { return nil, err } digest := hmac.New(digestAlg.New, secret) _, err = digest.Write(objBuf.Bytes()) if err != nil { return nil, err } n, err = encoding.Decode(buf, token[i+1:]) if err != nil { return nil, err } if !hmac.Equal(buf[:n], digest.Sum(nil)) { return nil, ErrMalformedToken } return obj, nil } |
Added internal/auth/impl/impl.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package impl provides services for authentification / authorization. package impl import ( "errors" "hash/fnv" "io" "time" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sexp" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/auth/policy" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" ) type myAuth struct { readonly bool owner id.Zid secret []byte } // New creates a new auth object. func New(readonly bool, owner id.Zid, extSecret string) auth.Manager { return &myAuth{ readonly: readonly, owner: owner, secret: calcSecret(extSecret), } } var configKeys = []string{ kernel.CoreProgname, kernel.CoreGoVersion, kernel.CoreHostname, kernel.CoreGoOS, kernel.CoreGoArch, kernel.CoreVersion, } func calcSecret(extSecret string) []byte { h := fnv.New128() if extSecret != "" { io.WriteString(h, extSecret) } for _, key := range configKeys { io.WriteString(h, kernel.Main.GetConfig(kernel.CoreService, key).(string)) } return h.Sum(nil) } // IsReadonly returns true, if the systems is configured to run in read-only-mode. func (a *myAuth) IsReadonly() bool { return a.readonly } // ErrMalformedToken signals a broken token. var ErrMalformedToken = errors.New("auth: malformed token") // ErrNoIdent signals that the 'ident' key is missing. var ErrNoIdent = errors.New("auth: missing ident") // ErrOtherKind signals that the token was defined for another token kind. var ErrOtherKind = errors.New("auth: wrong token kind") // ErrNoZid signals that the 'zid' key is missing. var ErrNoZid = errors.New("auth: missing zettel id") // GetToken returns a token to be used for authentification. func (a *myAuth) GetToken(ident *meta.Meta, d time.Duration, kind auth.TokenKind) ([]byte, error) { subject, ok := ident.Get(meta.KeyUserID) if !ok || subject == "" { return nil, ErrNoIdent } now := time.Now().Round(time.Second) sClaim := sx.MakeList( sx.Int64(kind), sx.MakeString(string(subject)), sx.Int64(now.Unix()), sx.Int64(now.Add(d).Unix()), sx.Int64(ident.Zid), ) return sign(sClaim, a.secret) } // ErrTokenExpired signals an exired token var ErrTokenExpired = errors.New("auth: token expired") // CheckToken checks the validity of the token and returns relevant data. func (a *myAuth) CheckToken(tok []byte, k auth.TokenKind) (auth.TokenData, error) { var tokenData auth.TokenData obj, err := check(tok, a.secret) if err != nil { return tokenData, err } tokenData.Token = tok err = setupTokenData(obj, k, &tokenData) return tokenData, err } func setupTokenData(obj sx.Object, k auth.TokenKind, tokenData *auth.TokenData) error { vals, err := sexp.ParseList(obj, "isiii") if err != nil { return ErrMalformedToken } if auth.TokenKind(vals[0].(sx.Int64)) != k { return ErrOtherKind } ident := vals[1].(sx.String).GetValue() if ident == "" { return ErrNoIdent } issued := time.Unix(int64(vals[2].(sx.Int64)), 0) expires := time.Unix(int64(vals[3].(sx.Int64)), 0) now := time.Now().Round(time.Second) if expires.Before(now) { return ErrTokenExpired } zid := id.Zid(vals[4].(sx.Int64)) if !zid.IsValid() { return ErrNoZid } tokenData.Ident = string(ident) tokenData.Issued = issued tokenData.Now = now tokenData.Expires = expires tokenData.Zid = zid return nil } func (a *myAuth) Owner() id.Zid { return a.owner } func (a *myAuth) IsOwner(zid id.Zid) bool { return zid.IsValid() && zid == a.owner } func (a *myAuth) WithAuth() bool { return a.owner != id.Invalid } // GetUserRole role returns the user role of the given user zettel. func (a *myAuth) GetUserRole(user *meta.Meta) meta.UserRole { if user == nil { if a.WithAuth() { return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } if val, ok := user.Get(meta.KeyUserRole); ok { if ur := val.AsUserRole(); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } func (a *myAuth) BoxWithPolicy(unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) { return policy.BoxWithPolicy(a, unprotectedBox, rtConfig) } |
Added internal/auth/policy/anon.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/config" ) type anonPolicy struct { authConfig config.AuthConfig pre auth.Policy } func (ap *anonPolicy) CanCreate(user, newMeta *meta.Meta) bool { return ap.pre.CanCreate(user, newMeta) } func (ap *anonPolicy) CanRead(user, m *meta.Meta) bool { return ap.pre.CanRead(user, m) && ap.checkVisibility(m) } func (ap *anonPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return ap.pre.CanWrite(user, oldMeta, newMeta) && ap.checkVisibility(oldMeta) } func (ap *anonPolicy) CanDelete(user, m *meta.Meta) bool { return ap.pre.CanDelete(user, m) && ap.checkVisibility(m) } func (ap *anonPolicy) CanRefresh(user *meta.Meta) bool { if ap.authConfig.GetExpertMode() || ap.authConfig.GetSimpleMode() { return true } return ap.pre.CanRefresh(user) } func (ap *anonPolicy) checkVisibility(m *meta.Meta) bool { if ap.authConfig.GetVisibility(m) == meta.VisibilityExpert { return ap.authConfig.GetExpertMode() } return true } |
Added internal/auth/policy/box.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/web/server" "zettelstore.de/z/internal/zettel" ) // BoxWithPolicy wraps the given box inside a policy box. func BoxWithPolicy(manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig) (box.Box, auth.Policy) { pol := newPolicy(manager, authConfig) return newBox(box, pol), pol } // polBox implements a policy box. type polBox struct { box box.Box policy auth.Policy } // newBox creates a new policy box. func newBox(box box.Box, policy auth.Policy) box.Box { return &polBox{ box: box, policy: policy, } } func (pp *polBox) Location() string { return pp.box.Location() } func (pp *polBox) CanCreateZettel(ctx context.Context) bool { return pp.box.CanCreateZettel(ctx) } func (pp *polBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { user := server.GetUser(ctx) if pp.policy.CanCreate(user, zettel.Meta) { return pp.box.CreateZettel(ctx, zettel) } return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid) } func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { z, err := pp.box.GetZettel(ctx, zid) if err != nil { return zettel.Zettel{}, err } user := server.GetUser(ctx) if pp.policy.CanRead(user, z.Meta) { return z, nil } return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) } func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { return pp.box.GetAllZettel(ctx, zid) } func (pp *polBox) FetchZids(ctx context.Context) (*idset.Set, error) { return nil, box.NewErrNotAllowed("fetch-zids", server.GetUser(ctx), id.Invalid) } func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.box.GetMeta(ctx, zid) if err != nil { return nil, err } user := server.GetUser(ctx) if pp.policy.CanRead(user, m) { return m, nil } return nil, box.NewErrNotAllowed("GetMeta", user, zid) } func (pp *polBox) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) { user := server.GetUser(ctx) canRead := pp.policy.CanRead q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) return pp.box.SelectMeta(ctx, metaSeq, q) } func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool { return pp.box.CanUpdateZettel(ctx, zettel) } func (pp *polBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { zid := zettel.Meta.Zid user := server.GetUser(ctx) if !zid.IsValid() { return box.ErrInvalidZid{Zid: zid.String()} } // Write existing zettel oldZettel, err := pp.box.GetZettel(ctx, zid) if err != nil { return err } if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) { return pp.box.UpdateZettel(ctx, zettel) } return box.NewErrNotAllowed("Write", user, zid) } func (pp *polBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return pp.box.CanDeleteZettel(ctx, zid) } func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error { z, err := pp.box.GetZettel(ctx, zid) if err != nil { return err } user := server.GetUser(ctx) if pp.policy.CanDelete(user, z.Meta) { return pp.box.DeleteZettel(ctx, zid) } return box.NewErrNotAllowed("Delete", user, zid) } func (pp *polBox) Refresh(ctx context.Context) error { user := server.GetUser(ctx) if pp.policy.CanRefresh(user) { return pp.box.Refresh(ctx) } return box.NewErrNotAllowed("Refresh", user, id.Invalid) } func (pp *polBox) ReIndex(ctx context.Context, zid id.Zid) error { user := server.GetUser(ctx) if pp.policy.CanRefresh(user) { // If a user is allowed to refresh all data, it it also allowed to re-index a zettel. return pp.box.ReIndex(ctx, zid) } return box.NewErrNotAllowed("ReIndex", user, zid) } |
Added internal/auth/policy/default.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" ) type defaultPolicy struct { manager auth.AuthzManager } func (*defaultPolicy) CanCreate(_, _ *meta.Meta) bool { return true } func (*defaultPolicy) CanRead(_, _ *meta.Meta) bool { return true } func (d *defaultPolicy) CanWrite(user, oldMeta, _ *meta.Meta) bool { return d.canChange(user, oldMeta) } func (d *defaultPolicy) CanDelete(user, m *meta.Meta) bool { return d.canChange(user, m) } func (*defaultPolicy) CanRefresh(user *meta.Meta) bool { return user != nil } func (d *defaultPolicy) canChange(user, m *meta.Meta) bool { metaRo, ok := m.Get(meta.KeyReadOnly) if !ok { return true } if user == nil { // If we are here, there is no authentication. // See owner.go:CanWrite. // No authentication: check for owner-like restriction, because the user // acts as an owner return metaRo != meta.ValueUserRoleOwner && !metaRo.AsBool() } userRole := d.manager.GetUserRole(user) switch metaRo { case meta.ValueUserRoleReader: return userRole > meta.UserRoleReader case meta.ValueUserRoleWriter: return userRole > meta.UserRoleWriter case meta.ValueUserRoleOwner: return userRole > meta.UserRoleOwner } return !metaRo.AsBool() } |
Added internal/auth/policy/owner.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/config" ) type ownerPolicy struct { manager auth.AuthzManager authConfig config.AuthConfig pre auth.Policy } func (o *ownerPolicy) CanCreate(user, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanCreate(user, newMeta) { return false } return o.userIsOwner(user) || o.userCanCreate(user, newMeta) } func (o *ownerPolicy) userCanCreate(user, newMeta *meta.Meta) bool { if o.manager.GetUserRole(user) == meta.UserRoleReader { return false } if _, ok := newMeta.Get(meta.KeyUserID); ok { return false } return true } func (o *ownerPolicy) CanRead(user, m *meta.Meta) bool { // No need to call o.pre.CanRead(user, meta), because it will always return true. // Both the default and the readonly policy allow to read a zettel. vis := o.authConfig.GetVisibility(m) if res, ok := o.checkVisibility(user, vis); ok { return res } return o.userIsOwner(user) || o.userCanRead(user, m, vis) } func (o *ownerPolicy) userCanRead(user, m *meta.Meta, vis meta.Visibility) bool { switch vis { case meta.VisibilityOwner, meta.VisibilityExpert: return false case meta.VisibilityPublic: return true } if user == nil { return false } if _, ok := m.Get(meta.KeyUserID); ok { // Only the user can read its own zettel return user.Zid == m.Zid } switch o.manager.GetUserRole(user) { case meta.UserRoleReader, meta.UserRoleWriter, meta.UserRoleOwner: return true case meta.UserRoleCreator: return vis == meta.VisibilityCreator default: return false } } var noChangeUser = []string{ meta.KeyID, meta.KeyRole, meta.KeyUserID, meta.KeyUserRole, } func (o *ownerPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) { return false } vis := o.authConfig.GetVisibility(oldMeta) if res, ok := o.checkVisibility(user, vis); ok { return res } if o.userIsOwner(user) { return true } if !o.userCanRead(user, oldMeta, vis) { return false } if _, ok := oldMeta.Get(meta.KeyUserID); ok { // Here we know, that user.Zid == newMeta.Zid (because of userCanRead) and // user.Zid == newMeta.Zid (because oldMeta.Zid == newMeta.Zid) for _, key := range noChangeUser { if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") { return false } } return true } switch userRole := o.manager.GetUserRole(user); userRole { case meta.UserRoleReader, meta.UserRoleCreator: return false } return o.userCanCreate(user, newMeta) } func (o *ownerPolicy) CanDelete(user, m *meta.Meta) bool { if user == nil || !o.pre.CanDelete(user, m) { return false } if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok { return res } return o.userIsOwner(user) } func (o *ownerPolicy) CanRefresh(user *meta.Meta) bool { switch userRole := o.manager.GetUserRole(user); userRole { case meta.UserRoleUnknown: return o.authConfig.GetSimpleMode() case meta.UserRoleCreator: return o.authConfig.GetExpertMode() || o.authConfig.GetSimpleMode() } return true } func (o *ownerPolicy) checkVisibility(user *meta.Meta, vis meta.Visibility) (bool, bool) { if vis == meta.VisibilityExpert { return o.userIsOwner(user) && o.authConfig.GetExpertMode(), true } return false, false } func (o *ownerPolicy) userIsOwner(user *meta.Meta) bool { if user == nil { return false } if o.manager.IsOwner(user.Zid) { return true } if val, ok := user.Get(meta.KeyUserRole); ok && val == meta.ValueUserRoleOwner { return true } return false } |
Added internal/auth/policy/policy.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/config" ) // newPolicy creates a policy based on given constraints. func newPolicy(manager auth.AuthzManager, authConfig config.AuthConfig) auth.Policy { var pol auth.Policy if manager.IsReadonly() { pol = &roPolicy{} } else { pol = &defaultPolicy{manager} } if manager.WithAuth() { pol = &ownerPolicy{ manager: manager, authConfig: authConfig, pre: pol, } } else { pol = &anonPolicy{ authConfig: authConfig, pre: pol, } } return &prePolicy{pol} } type prePolicy struct { post auth.Policy } func (p *prePolicy) CanCreate(user, newMeta *meta.Meta) bool { return newMeta != nil && p.post.CanCreate(user, newMeta) } func (p *prePolicy) CanRead(user, m *meta.Meta) bool { return m != nil && p.post.CanRead(user, m) } func (p *prePolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return oldMeta != nil && newMeta != nil && oldMeta.Zid == newMeta.Zid && p.post.CanWrite(user, oldMeta, newMeta) } func (p *prePolicy) CanDelete(user, m *meta.Meta) bool { return m != nil && p.post.CanDelete(user, m) } func (p *prePolicy) CanRefresh(user *meta.Meta) bool { return p.post.CanRefresh(user) } |
Added internal/auth/policy/policy_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( "fmt" "testing" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" ) func TestPolicies(t *testing.T) { t.Parallel() testScene := []struct { readonly bool withAuth bool expert bool simple bool }{ {true, true, true, true}, {true, true, true, false}, {true, true, false, true}, {true, true, false, false}, {true, false, true, true}, {true, false, true, false}, {true, false, false, true}, {true, false, false, false}, {false, true, true, true}, {false, true, true, false}, {false, true, false, true}, {false, true, false, false}, {false, false, true, true}, {false, false, true, false}, {false, false, false, true}, {false, false, false, false}, } for _, ts := range testScene { pol := newPolicy( &testAuthzManager{readOnly: ts.readonly, withAuth: ts.withAuth}, &authConfig{simple: ts.simple, expert: ts.expert}, ) name := fmt.Sprintf("readonly=%v/withauth=%v/expert=%v/simple=%v", ts.readonly, ts.withAuth, ts.expert, ts.simple) t.Run(name, func(tt *testing.T) { testCreate(tt, pol, ts.withAuth, ts.readonly) testRead(tt, pol, ts.withAuth, ts.expert) testWrite(tt, pol, ts.withAuth, ts.readonly, ts.expert) testDelete(tt, pol, ts.withAuth, ts.readonly, ts.expert) testRefresh(tt, pol, ts.withAuth, ts.expert, ts.simple) }) } } type testAuthzManager struct { readOnly bool withAuth bool } func (a *testAuthzManager) IsReadonly() bool { return a.readOnly } func (*testAuthzManager) Owner() id.Zid { return ownerZid } func (*testAuthzManager) IsOwner(zid id.Zid) bool { return zid == ownerZid } func (a *testAuthzManager) WithAuth() bool { return a.withAuth } func (a *testAuthzManager) GetUserRole(user *meta.Meta) meta.UserRole { if user == nil { if a.WithAuth() { return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } if val, ok := user.Get(meta.KeyUserRole); ok { if ur := val.AsUserRole(); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } type authConfig struct{ simple, expert bool } func (ac *authConfig) GetSimpleMode() bool { return ac.simple } func (ac *authConfig) GetExpertMode() bool { return ac.expert } func (*authConfig) GetVisibility(m *meta.Meta) meta.Visibility { if val, ok := m.Get(meta.KeyVisibility); ok { return val.AsVisibility() } return meta.VisibilityLogin } func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly bool) { t.Helper() anonUser := newAnon() creator := newCreator() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() userZettel := newUserZettel() testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {creator, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Ordinary zettel {anonUser, zettel, !withAuth && !readonly}, {creator, zettel, !readonly}, {reader, zettel, !withAuth && !readonly}, {writer, zettel, !readonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // User zettel {anonUser, userZettel, !withAuth && !readonly}, {creator, userZettel, !withAuth && !readonly}, {reader, userZettel, !withAuth && !readonly}, {writer, userZettel, !withAuth && !readonly}, {owner, userZettel, !readonly}, {owner2, userZettel, !readonly}, } for _, tc := range testCases { t.Run("Create", func(tt *testing.T) { got := pol.CanCreate(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testRead(t *testing.T, pol auth.Policy, withAuth, expert bool) { t.Helper() anonUser := newAnon() creator := newCreator() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() publicZettel := newPublicZettel() creatorZettel := newCreatorZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() userZettel := newUserZettel() testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {creator, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Ordinary zettel {anonUser, zettel, !withAuth}, {creator, zettel, !withAuth}, {reader, zettel, true}, {writer, zettel, true}, {owner, zettel, true}, {owner2, zettel, true}, // Public zettel {anonUser, publicZettel, true}, {creator, publicZettel, true}, {reader, publicZettel, true}, {writer, publicZettel, true}, {owner, publicZettel, true}, {owner2, publicZettel, true}, // Creator zettel {anonUser, creatorZettel, !withAuth}, {creator, creatorZettel, true}, {reader, creatorZettel, true}, {writer, creatorZettel, true}, {owner, creatorZettel, true}, {owner2, creatorZettel, true}, // Login zettel {anonUser, loginZettel, !withAuth}, {creator, loginZettel, !withAuth}, {reader, loginZettel, true}, {writer, loginZettel, true}, {owner, loginZettel, true}, {owner2, loginZettel, true}, // Owner zettel {anonUser, ownerZettel, !withAuth}, {creator, ownerZettel, !withAuth}, {reader, ownerZettel, !withAuth}, {writer, ownerZettel, !withAuth}, {owner, ownerZettel, true}, {owner2, ownerZettel, true}, // Expert zettel {anonUser, expertZettel, !withAuth && expert}, {creator, expertZettel, !withAuth && expert}, {reader, expertZettel, !withAuth && expert}, {writer, expertZettel, !withAuth && expert}, {owner, expertZettel, expert}, {owner2, expertZettel, expert}, // Other user zettel {anonUser, userZettel, !withAuth}, {creator, userZettel, !withAuth}, {reader, userZettel, !withAuth}, {writer, userZettel, !withAuth}, {owner, userZettel, true}, {owner2, userZettel, true}, // Own user zettel {creator, creator, true}, {reader, reader, true}, {writer, writer, true}, {owner, owner, true}, {owner, owner2, true}, {owner2, owner, true}, {owner2, owner2, true}, } for _, tc := range testCases { t.Run("Read", func(tt *testing.T) { got := pol.CanRead(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testWrite(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() creator := newCreator() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() userZettel := newUserZettel() writerNew := writer.Clone() writerNew.Set(meta.KeyUserRole, owner.GetDefault(meta.KeyUserRole, "")) roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() notAuthNotReadonly := !withAuth && !readonly testCases := []struct { user *meta.Meta old *meta.Meta new *meta.Meta exp bool }{ // No old and new meta {anonUser, nil, nil, false}, {creator, nil, nil, false}, {reader, nil, nil, false}, {writer, nil, nil, false}, {owner, nil, nil, false}, {owner2, nil, nil, false}, // No old meta {anonUser, nil, zettel, false}, {creator, nil, zettel, false}, {reader, nil, zettel, false}, {writer, nil, zettel, false}, {owner, nil, zettel, false}, {owner2, nil, zettel, false}, // No new meta {anonUser, zettel, nil, false}, {creator, zettel, nil, false}, {reader, zettel, nil, false}, {writer, zettel, nil, false}, {owner, zettel, nil, false}, {owner2, zettel, nil, false}, // Old an new zettel have different zettel identifier {anonUser, zettel, publicZettel, false}, {creator, zettel, publicZettel, false}, {reader, zettel, publicZettel, false}, {writer, zettel, publicZettel, false}, {owner, zettel, publicZettel, false}, {owner2, zettel, publicZettel, false}, // Overwrite a normal zettel {anonUser, zettel, zettel, notAuthNotReadonly}, {creator, zettel, zettel, notAuthNotReadonly}, {reader, zettel, zettel, notAuthNotReadonly}, {writer, zettel, zettel, !readonly}, {owner, zettel, zettel, !readonly}, {owner2, zettel, zettel, !readonly}, // Public zettel {anonUser, publicZettel, publicZettel, notAuthNotReadonly}, {creator, publicZettel, publicZettel, notAuthNotReadonly}, {reader, publicZettel, publicZettel, notAuthNotReadonly}, {writer, publicZettel, publicZettel, !readonly}, {owner, publicZettel, publicZettel, !readonly}, {owner2, publicZettel, publicZettel, !readonly}, // Login zettel {anonUser, loginZettel, loginZettel, notAuthNotReadonly}, {creator, loginZettel, loginZettel, notAuthNotReadonly}, {reader, loginZettel, loginZettel, notAuthNotReadonly}, {writer, loginZettel, loginZettel, !readonly}, {owner, loginZettel, loginZettel, !readonly}, {owner2, loginZettel, loginZettel, !readonly}, // Owner zettel {anonUser, ownerZettel, ownerZettel, notAuthNotReadonly}, {creator, ownerZettel, ownerZettel, notAuthNotReadonly}, {reader, ownerZettel, ownerZettel, notAuthNotReadonly}, {writer, ownerZettel, ownerZettel, notAuthNotReadonly}, {owner, ownerZettel, ownerZettel, !readonly}, {owner2, ownerZettel, ownerZettel, !readonly}, // Expert zettel {anonUser, expertZettel, expertZettel, notAuthNotReadonly && expert}, {creator, expertZettel, expertZettel, notAuthNotReadonly && expert}, {reader, expertZettel, expertZettel, notAuthNotReadonly && expert}, {writer, expertZettel, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, expertZettel, !readonly && expert}, {owner2, expertZettel, expertZettel, !readonly && expert}, // Other user zettel {anonUser, userZettel, userZettel, notAuthNotReadonly}, {creator, userZettel, userZettel, notAuthNotReadonly}, {reader, userZettel, userZettel, notAuthNotReadonly}, {writer, userZettel, userZettel, notAuthNotReadonly}, {owner, userZettel, userZettel, !readonly}, {owner2, userZettel, userZettel, !readonly}, // Own user zettel {creator, creator, creator, !readonly}, {reader, reader, reader, !readonly}, {writer, writer, writer, !readonly}, {owner, owner, owner, !readonly}, {owner2, owner2, owner2, !readonly}, // Writer cannot change importand metadata of its own user zettel {writer, writer, writerNew, notAuthNotReadonly}, // No r/o zettel {anonUser, roFalse, roFalse, notAuthNotReadonly}, {creator, roFalse, roFalse, notAuthNotReadonly}, {reader, roFalse, roFalse, notAuthNotReadonly}, {writer, roFalse, roFalse, !readonly}, {owner, roFalse, roFalse, !readonly}, {owner2, roFalse, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, roReader, false}, {creator, roReader, roReader, false}, {reader, roReader, roReader, false}, {writer, roReader, roReader, !readonly}, {owner, roReader, roReader, !readonly}, {owner2, roReader, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, roWriter, false}, {creator, roWriter, roWriter, false}, {reader, roWriter, roWriter, false}, {writer, roWriter, roWriter, false}, {owner, roWriter, roWriter, !readonly}, {owner2, roWriter, roWriter, !readonly}, // Owner r/o zettel {anonUser, roOwner, roOwner, false}, {creator, roOwner, roOwner, false}, {reader, roOwner, roOwner, false}, {writer, roOwner, roOwner, false}, {owner, roOwner, roOwner, false}, {owner2, roOwner, roOwner, false}, // r/o = true zettel {anonUser, roTrue, roTrue, false}, {creator, roTrue, roTrue, false}, {reader, roTrue, roTrue, false}, {writer, roTrue, roTrue, false}, {owner, roTrue, roTrue, false}, {owner2, roTrue, roTrue, false}, } for _, tc := range testCases { t.Run("Write", func(tt *testing.T) { got := pol.CanWrite(tc.user, tc.old, tc.new) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testDelete(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() creator := newCreator() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() notAuthNotReadonly := !withAuth && !readonly testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {creator, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Any zettel {anonUser, zettel, notAuthNotReadonly}, {creator, zettel, notAuthNotReadonly}, {reader, zettel, notAuthNotReadonly}, {writer, zettel, notAuthNotReadonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel {anonUser, expertZettel, notAuthNotReadonly && expert}, {creator, expertZettel, notAuthNotReadonly && expert}, {reader, expertZettel, notAuthNotReadonly && expert}, {writer, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, // No r/o zettel {anonUser, roFalse, notAuthNotReadonly}, {creator, roFalse, notAuthNotReadonly}, {reader, roFalse, notAuthNotReadonly}, {writer, roFalse, notAuthNotReadonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {creator, roReader, false}, {reader, roReader, false}, {writer, roReader, notAuthNotReadonly}, {owner, roReader, !readonly}, {owner2, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, false}, {creator, roWriter, false}, {reader, roWriter, false}, {writer, roWriter, false}, {owner, roWriter, !readonly}, {owner2, roWriter, !readonly}, // Owner r/o zettel {anonUser, roOwner, false}, {creator, roOwner, false}, {reader, roOwner, false}, {writer, roOwner, false}, {owner, roOwner, false}, {owner2, roOwner, false}, // r/o = true zettel {anonUser, roTrue, false}, {creator, roTrue, false}, {reader, roTrue, false}, {writer, roTrue, false}, {owner, roTrue, false}, {owner2, roTrue, false}, } for _, tc := range testCases { t.Run("Delete", func(tt *testing.T) { got := pol.CanDelete(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testRefresh(t *testing.T, pol auth.Policy, withAuth, expert, simple bool) { t.Helper() testCases := []struct { user *meta.Meta exp bool }{ {newAnon(), (!withAuth && expert) || simple}, {newCreator(), !withAuth || expert || simple}, {newReader(), true}, {newWriter(), true}, {newOwner(), true}, {newOwner2(), true}, } for _, tc := range testCases { t.Run("Refresh", func(tt *testing.T) { got := pol.CanRefresh(tc.user) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } const ( creatorZid = id.Zid(1013) readerZid = id.Zid(1013) writerZid = id.Zid(1015) ownerZid = id.Zid(1017) owner2Zid = id.Zid(1019) zettelZid = id.Zid(1021) visZid = id.Zid(1023) userZid = id.Zid(1025) ) func newAnon() *meta.Meta { return nil } func newCreator() *meta.Meta { user := meta.New(creatorZid) user.Set(meta.KeyTitle, "Creator") user.Set(meta.KeyUserID, "ceator") user.Set(meta.KeyUserRole, meta.ValueUserRoleCreator) return user } func newReader() *meta.Meta { user := meta.New(readerZid) user.Set(meta.KeyTitle, "Reader") user.Set(meta.KeyUserID, "reader") user.Set(meta.KeyUserRole, meta.ValueUserRoleReader) return user } func newWriter() *meta.Meta { user := meta.New(writerZid) user.Set(meta.KeyTitle, "Writer") user.Set(meta.KeyUserID, "writer") user.Set(meta.KeyUserRole, meta.ValueUserRoleWriter) return user } func newOwner() *meta.Meta { user := meta.New(ownerZid) user.Set(meta.KeyTitle, "Owner") user.Set(meta.KeyUserID, "owner") user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner) return user } func newOwner2() *meta.Meta { user := meta.New(owner2Zid) user.Set(meta.KeyTitle, "Owner 2") user.Set(meta.KeyUserID, "owner-2") user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner) return user } func newZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "Any Zettel") return m } func newPublicZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Public Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } func newCreatorZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Creator Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityCreator) return m } func newLoginZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Login Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func newOwnerZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Owner Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityOwner) return m } func newExpertZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Expert Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func newRoFalseZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "No r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueFalse) return m } func newRoTrueZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "A r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueTrue) return m } func newRoReaderZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "Reader r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueUserRoleReader) return m } func newRoWriterZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "Writer r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueUserRoleWriter) return m } func newRoOwnerZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "Owner r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueUserRoleOwner) return m } func newUserZettel() *meta.Meta { m := meta.New(userZid) m.Set(meta.KeyTitle, "Any User") m.Set(meta.KeyUserID, "any") return m } |
Added internal/auth/policy/readonly.go.
> > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import "t73f.de/r/zsc/domain/meta" type roPolicy struct{} func (*roPolicy) CanCreate(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanRead(_, _ *meta.Meta) bool { return true } func (*roPolicy) CanWrite(_, _, _ *meta.Meta) bool { return false } func (*roPolicy) CanDelete(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanRefresh(user *meta.Meta) bool { return user != nil } |
Added internal/box/box.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package box provides a generic interface to zettel boxes. package box import ( "context" "errors" "fmt" "io" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) // BaseBox is implemented by all Zettel boxes. type BaseBox interface { // Location returns some information where the box is located. // Format is dependent of the box. Location() string // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) // CanDeleteZettel returns true, if box could possibly delete the given zettel. CanDeleteZettel(ctx context.Context, zid id.Zid) bool // DeleteZettel removes the zettel from the box. DeleteZettel(ctx context.Context, zid id.Zid) error } // WriteBox is a box that can create / update zettel content. type WriteBox interface { // CanCreateZettel returns true, if box could possibly create a new zettel. CanCreateZettel(ctx context.Context) bool // CreateZettel creates a new zettel. // Returns the new zettel id (and an error indication). CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) // CanUpdateZettel returns true, if box could possibly update the given zettel. CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool // UpdateZettel updates an existing zettel. UpdateZettel(ctx context.Context, zettel zettel.Zettel) error } // ZidFunc is a function that processes identifier of a zettel. type ZidFunc func(id.Zid) // MetaFunc is a function that processes metadata of a zettel. type MetaFunc func(*meta.Meta) // ManagedBox is the interface of managed boxes. type ManagedBox interface { BaseBox // HasZettel returns true, if box conains zettel with given identifier. HasZettel(context.Context, id.Zid) bool // Apply identifier of every zettel to the given function, if predicate returns true. ApplyZid(context.Context, ZidFunc, query.RetrievePredicate) error // Apply metadata of every zettel to the given function, if predicate returns true. ApplyMeta(context.Context, MetaFunc, query.RetrievePredicate) error // ReadStats populates st with box statistics ReadStats(st *ManagedBoxStats) } // ManagedBoxStats records statistics about the box. type ManagedBoxStats struct { // ReadOnly indicates that the content of a box cannot change. ReadOnly bool // Zettel is the number of zettel managed by the box. Zettel int } // StartState enumerates the possible states of starting and stopping a box. // // StartStateStopped -> StartStateStarting -> StartStateStarted -> StateStateStopping -> StartStateStopped. // // Other transitions are also possible. type StartState uint8 // Constant values of StartState const ( StartStateStopped StartState = iota StartStateStarting StartStateStarted StartStateStopping ) // StartStopper performs simple lifecycle management. type StartStopper interface { // State the current status of the box. State() StartState // Start the box. Now all other functions of the box are allowed. // Starting a box, which is not in state StartStateStopped is not allowed. Start(ctx context.Context) error // Stop the started box. Now only the Start() function is allowed. Stop(ctx context.Context) } // Refresher allow to refresh their internal data. type Refresher interface { // Refresh the box data. Refresh(context.Context) } // Box is to be used outside the box package and its descendants. type Box interface { BaseBox WriteBox // FetchZids returns the set of all zettel identifer managed by the box. FetchZids(ctx context.Context) (*idset.Set, error) // GetMeta returns the metadata of the zettel with the given identifier. GetMeta(context.Context, id.Zid) (*meta.Meta, error) // SelectMeta returns a list of metadata that comply to the given selection criteria. // If `metaSeq` is `nil`, the box assumes metadata of all available zettel. SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) // GetAllZettel retrieves a specific zettel from all managed boxes. GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) // Refresh the data from the box and from its managed sub-boxes. Refresh(context.Context) error // ReIndex one zettel to update its index data. ReIndex(context.Context, id.Zid) error } // Stats record stattistics about a box. type Stats struct { // ReadOnly indicates that boxes cannot be modified. ReadOnly bool // NumManagedBoxes is the number of boxes managed. NumManagedBoxes int // Zettel is the number of zettel managed by the box, including // duplicates across managed boxes. ZettelTotal int // LastReload stores the timestamp when a full re-index was done. LastReload time.Time // DurLastReload is the duration of the last full re-index run. DurLastReload time.Duration // IndexesSinceReload counts indexing a zettel since the full re-index. IndexesSinceReload uint64 // ZettelIndexed is the number of zettel managed by the indexer. ZettelIndexed int // IndexUpdates count the number of metadata updates. IndexUpdates uint64 // IndexedWords count the different words indexed. IndexedWords uint64 // IndexedUrls count the different URLs indexed. IndexedUrls uint64 } // Manager is a box-managing box. type Manager interface { Box StartStopper Subject // ReadStats populates st with box statistics ReadStats(st *Stats) // Dump internal data to a Writer. Dump(w io.Writer) } // UpdateReason gives an indication, why the ObserverFunc was called. type UpdateReason uint8 // Values for Reason const ( _ UpdateReason = iota OnReady // Box is started and fully operational OnReload // Box was reloaded OnZettel // Something with an existing zettel happened OnDelete // A zettel was deleted ) // UpdateInfo contains all the data about a changed zettel. type UpdateInfo struct { Box BaseBox Reason UpdateReason Zid id.Zid } // UpdateFunc is a function to be called when a change is detected. type UpdateFunc func(UpdateInfo) // UpdateNotifier is an UpdateFunc, but with separate values. type UpdateNotifier func(BaseBox, id.Zid, UpdateReason) // Subject is a box that notifies observers about changes. type Subject interface { // RegisterObserver registers an observer that will be notified // if one or all zettel are found to be changed. RegisterObserver(UpdateFunc) } // Enricher is used to update metadata by adding new properties. type Enricher interface { // Enrich computes additional properties and updates the given metadata. // It is typically called by zettel reading methods. Enrich(ctx context.Context, m *meta.Meta, boxNumber int) } // NoEnrichContext will signal an enricher that nothing has to be done. // This is useful for an Indexer, but also for some box.Box calls, when // just the plain metadata is needed. func NoEnrichContext(ctx context.Context) context.Context { return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey) } type ctxNoEnrichType struct{} var ctxNoEnrichKey ctxNoEnrichType // DoEnrich determines if the context is not marked to not enrich metadata. func DoEnrich(ctx context.Context) bool { _, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType) return !ok } // NoEnrichQuery provides a context that signals not to enrich, if the query does not need this. func NoEnrichQuery(ctx context.Context, q *query.Query) context.Context { if q.EnrichNeeded() { return ctx } return NoEnrichContext(ctx) } // ErrNotAllowed is returned if the caller is not allowed to perform the operation. type ErrNotAllowed struct { Op string User *meta.Meta Zid id.Zid } // NewErrNotAllowed creates an new authorization error. func NewErrNotAllowed(op string, user *meta.Meta, zid id.Zid) error { return &ErrNotAllowed{ Op: op, User: user, Zid: zid, } } func (err *ErrNotAllowed) Error() string { if err.User == nil { if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for not authorized user", err.Op, err.Zid) } return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op) } if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for user %v/%v", err.Op, err.Zid, err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid) } return fmt.Sprintf( "operation %q not allowed for user %v/%v", err.Op, err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid) } // Is return true, if the error is of type ErrNotAllowed. func (*ErrNotAllowed) Is(error) bool { return true } // ErrStarted is returned when trying to start an already started box. var ErrStarted = errors.New("box is already started") // ErrStopped is returned if calling methods on a box that was not started. var ErrStopped = errors.New("box is stopped") // ErrReadOnly is returned if there is an attepmt to write to a read-only box. var ErrReadOnly = errors.New("read-only box") // ErrZettelNotFound is returned if a zettel was not found in the box. type ErrZettelNotFound struct{ Zid id.Zid } func (eznf ErrZettelNotFound) Error() string { return "zettel not found: " + eznf.Zid.String() } // ErrConflict is returned if a box operation detected a conflict.. // One example: if calculating a new zettel identifier takes too long. var ErrConflict = errors.New("conflict") // ErrCapacity is returned if a box has reached its capacity. var ErrCapacity = errors.New("capacity exceeded") // ErrInvalidZid is returned if the zettel id is not appropriate for the box operation. type ErrInvalidZid struct{ Zid string } func (err ErrInvalidZid) Error() string { return "invalid Zettel id: " + err.Zid } |
Added internal/box/compbox/compbox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) func init() { manager.Register( " comp", func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return getCompBox(cdata.Number, cdata.Enricher), nil }) } type compBox struct { log *logger.Logger number int enricher box.Enricher } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta content func(context.Context, *compBox) []byte }{ id.ZidVersion: {genVersionBuildM, genVersionBuildC}, id.ZidHost: {genVersionHostM, genVersionHostC}, id.ZidOperatingSystem: {genVersionOSM, genVersionOSC}, id.ZidLog: {genLogM, genLogC}, id.ZidMemory: {genMemoryM, genMemoryC}, id.ZidSx: {genSxM, genSxC}, // id.ZidHTTP: {genHttpM, genHttpC}, // id.ZidAPI: {genApiM, genApiC}, // id.ZidWebUI: {genWebUiM, genWebUiC}, // id.ZidConsole: {genConsoleM, genConsoleC}, id.ZidBoxManager: {genManagerM, genManagerC}, // id.ZidIndex: {genIndexM, genIndexC}, // id.ZidQuery: {genQueryM, genQueryC}, id.ZidMetadataKey: {genKeysM, genKeysC}, id.ZidParser: {genParserM, genParserC}, id.ZidStartupConfiguration: {genConfigZettelM, genConfigZettelC}, } // Get returns the one program box. func getCompBox(boxNumber int, mf box.Enricher) *compBox { return &compBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "comp").Int("boxnum", int64(boxNumber)).Child(), number: boxNumber, enricher: mf, } } // Setup remembers important values. func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } func (*compBox) Location() string { return "" } func (cb *compBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { if gen, ok := myZettel[zid]; ok && gen.meta != nil { if m := gen.meta(zid); m != nil { updateMeta(m) if genContent := gen.content; genContent != nil { cb.log.Trace().Msg("GetZettel/Content") return zettel.Zettel{ Meta: m, Content: zettel.NewContent(genContent(ctx, cb)), }, nil } cb.log.Trace().Msg("GetZettel/NoContent") return zettel.Zettel{Meta: m}, nil } } err := box.ErrZettelNotFound{Zid: zid} cb.log.Trace().Err(err).Msg("GetZettel/Err") return zettel.Zettel{}, err } func (*compBox) HasZettel(_ context.Context, zid id.Zid) bool { _, found := myZettel[zid] return found } func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyZid") for zid, gen := range myZettel { if !constraint(zid) { continue } if genMeta := gen.meta; genMeta != nil { if genMeta(zid) != nil { handle(zid) } } } return nil } func (cb *compBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta") for zid, gen := range myZettel { if !constraint(zid) { continue } if genMeta := gen.meta; genMeta != nil { if m := genMeta(zid); m != nil { updateMeta(m) cb.enricher.Enrich(ctx, m, cb.number) handle(m) } } } return nil } func (*compBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { if _, ok := myZettel[zid]; ok { err = box.ErrReadOnly } else { err = box.ErrZettelNotFound{Zid: zid} } cb.log.Trace().Err(err).Msg("DeleteZettel") return err } func (cb *compBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(myZettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } func getTitledMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, meta.Value(title)) return m } func updateMeta(m *meta.Meta) { if _, ok := m.Get(meta.KeySyntax); !ok { m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) } m.Set(meta.KeyRole, meta.ValueRoleConfiguration) if _, ok := m.Get(meta.KeyCreated); !ok { m.Set(meta.KeyCreated, meta.Value(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))) } m.Set(meta.KeyLang, meta.ValueLangEN) m.Set(meta.KeyReadOnly, meta.ValueTrue) if _, ok := m.Get(meta.KeyVisibility); !ok { m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) } } |
Added internal/box/compbox/config.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myConfig == nil { return nil } return getTitledMeta(zid, "Zettelstore Startup Configuration") } func genConfigZettelC(context.Context, *compBox) []byte { var buf bytes.Buffer second := false for key, val := range myConfig.All() { if second { buf.WriteByte('\n') } second = true buf.WriteString("; ''") buf.WriteString(key) buf.WriteString("''") if val != "" { buf.WriteString("\n: ``") for _, r := range val { if r == '`' { buf.WriteByte('\\') } buf.WriteRune(r) } buf.WriteString("``") } } return buf.Bytes() } |
Added internal/box/compbox/keys.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" "context" "fmt" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/kernel" ) func genKeysM(zid id.Zid) *meta.Meta { m := getTitledMeta(zid, "Zettelstore Supported Metadata Keys") m.Set(meta.KeyCreated, meta.Value(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))) m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func genKeysC(context.Context, *compBox) []byte { keys := meta.GetSortedKeyDescriptions() var buf bytes.Buffer buf.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") for _, kd := range keys { fmt.Fprintf(&buf, "|[[%v|query:%v?]]|%v|%v|%v\n", kd.Name, kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) } return buf.Bytes() } |
Added internal/box/compbox/log.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/kernel" ) func genLogM(zid id.Zid) *meta.Meta { m := getTitledMeta(zid, "Zettelstore Log") m.Set(meta.KeySyntax, meta.ValueSyntaxText) m.Set(meta.KeyModified, meta.Value(kernel.Main.GetLastLogTime().Local().Format(id.TimestampLayout))) return m } func genLogC(context.Context, *compBox) []byte { const tsFormat = "2006-01-02 15:04:05.999999" entries := kernel.Main.RetrieveLogEntries() var buf bytes.Buffer for _, entry := range entries { ts := entry.TS.Format(tsFormat) buf.WriteString(ts) for j := len(ts); j < len(tsFormat); j++ { buf.WriteByte('0') } buf.WriteByte(' ') buf.WriteString(entry.Level.Format()) buf.WriteByte(' ') buf.WriteString(entry.Prefix) buf.WriteByte(' ') buf.WriteString(entry.Message) buf.WriteByte('\n') } return buf.Bytes() } |
Added internal/box/compbox/manager.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" "context" "fmt" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/kernel" ) func genManagerM(zid id.Zid) *meta.Meta { return getTitledMeta(zid, "Zettelstore Box Manager") } func genManagerC(context.Context, *compBox) []byte { kvl := kernel.Main.GetServiceStatistics(kernel.BoxService) if len(kvl) == 0 { return nil } var buf bytes.Buffer buf.WriteString("|=Name|=Value>\n") for _, kv := range kvl { fmt.Fprintf(&buf, "| %v | %v\n", kv.Key, kv.Value) } return buf.Bytes() } |
Added internal/box/compbox/memory.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | //----------------------------------------------------------------------------- // Copyright (c) 2024-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2024-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" "context" "fmt" "os" "runtime" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/kernel" ) func genMemoryM(zid id.Zid) *meta.Meta { return getTitledMeta(zid, "Zettelstore Memory") } func genMemoryC(context.Context, *compBox) []byte { pageSize := os.Getpagesize() var m runtime.MemStats runtime.GC() runtime.ReadMemStats(&m) var buf bytes.Buffer buf.WriteString("|=Name|=Value>\n") fmt.Fprintf(&buf, "|Page Size|%d\n", pageSize) fmt.Fprintf(&buf, "|Pages|%d\n", m.HeapSys/uint64(pageSize)) fmt.Fprintf(&buf, "|Heap Objects|%d\n", m.HeapObjects) fmt.Fprintf(&buf, "|Heap Sys (KiB)|%d\n", m.HeapSys/1024) fmt.Fprintf(&buf, "|Heap Inuse (KiB)|%d\n", m.HeapInuse/1024) fmt.Fprintf(&buf, "|CPUs|%d\n", runtime.NumCPU()) fmt.Fprintf(&buf, "|Threads|%d\n", runtime.NumGoroutine()) debug := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool) if debug { for i, bysize := range m.BySize { fmt.Fprintf(&buf, "|Size %2d: %d|%d - %d → %d\n", i, bysize.Size, bysize.Mallocs, bysize.Frees, bysize.Mallocs-bysize.Frees) } } return buf.Bytes() } |
Added internal/box/compbox/parser.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" "context" "fmt" "slices" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/parser" ) func genParserM(zid id.Zid) *meta.Meta { m := getTitledMeta(zid, "Zettelstore Supported Parser") m.Set(meta.KeyCreated, meta.Value(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))) m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func genParserC(context.Context, *compBox) []byte { var buf bytes.Buffer buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Text Format?:|=Image Format?:\n") syntaxes := parser.GetSyntaxes() slices.Sort(syntaxes) for _, syntax := range syntaxes { info := parser.Get(syntax) if info.Name != syntax { continue } altNames := info.AltNames slices.Sort(altNames) fmt.Fprintf( &buf, "|%v|%v|%v|%v|%v\n", syntax, strings.Join(altNames, ", "), info.IsASTParser, info.IsTextFormat, info.IsImageFormat) } return buf.Bytes() } |
Added internal/box/compbox/sx.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | //----------------------------------------------------------------------------- // Copyright (c) 2024-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2024-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" "context" "fmt" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" ) func genSxM(zid id.Zid) *meta.Meta { return getTitledMeta(zid, "Zettelstore Sx Engine") } func genSxC(context.Context, *compBox) []byte { var buf bytes.Buffer buf.WriteString("|=Name|=Value>\n") fmt.Fprintf(&buf, "|Symbols|%d\n", sx.MakeSymbol("NIL").Factory().Size()) return buf.Bytes() } |
Added internal/box/compbox/version.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/kernel" ) func genVersionBuildM(zid id.Zid) *meta.Meta { m := getTitledMeta(zid, "Zettelstore Version") m.Set(meta.KeyCreated, meta.Value(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))) m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func genVersionBuildC(context.Context, *compBox) []byte { return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) } func genVersionHostM(zid id.Zid) *meta.Meta { return getTitledMeta(zid, "Zettelstore Host") } func genVersionHostC(context.Context, *compBox) []byte { return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string)) } func genVersionOSM(zid id.Zid) *meta.Meta { return getTitledMeta(zid, "Zettelstore Operating System") } func genVersionOSC(context.Context, *compBox) []byte { goOS := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string) goArch := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string) result := make([]byte, 0, len(goOS)+len(goArch)+1) result = append(result, goOS...) result = append(result, '/') return append(result, goArch...) } |
Added internal/box/constbox/base.css.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | /*----------------------------------------------------------------------------- * Copyright (c) 2020-present Detlef Stern * * This file is part of Zettelstore. * * Zettelstore is licensed under the latest version of the EUPL (European Union * Public License). Please see file LICENSE.txt for your rights and obligations * under this license. * * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2020-present Detlef Stern *----------------------------------------------------------------------------- */ *,*::before,*::after { box-sizing: border-box; } html { font-family: serif; scroll-behavior: smooth; height: 100%; } body { margin: 0; min-height: 100vh; line-height: 1.4; background-color: #f8f8f8 ; height: 100%; } nav.zs-menu { background-color: hsl(210, 28%, 90%); overflow: auto; white-space: nowrap; font-family: sans-serif; padding-left: .5rem; } nav.zs-menu > a { float:left; display: block; text-align: center; padding:.41rem .5rem; text-decoration: none; color:black; } nav.zs-menu > a:hover, .zs-dropdown:hover button { background-color: hsl(210, 28%, 80%) } nav.zs-menu form { float: right } nav.zs-menu form input[type=text] { padding: .12rem; border: none; margin-top: .25rem; margin-right: .5rem; } .zs-dropdown { float: left; overflow: hidden; } .zs-dropdown > button { font-size: 16px; border: none; outline: none; color: black; padding:.41rem .5rem; background-color: inherit; font-family: inherit; margin: 0; } .zs-dropdown-content { display: none; position: absolute; background-color: #f9f9f9; min-width: 160px; box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); z-index: 1; } .zs-dropdown-content > a { float: none; color: black; padding:.41rem .5rem; text-decoration: none; display: block; text-align: left; } .zs-dropdown-content > a:hover { background-color: hsl(210, 28%, 75%) } .zs-dropdown:hover > .zs-dropdown-content { display: block } main { padding: 0 1rem } article > * + * { margin-top: .5rem } article header { padding: 0; margin: 0; } h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal; margin:.4em 0 } h1 { font-size:1.5em } h2 { font-size:1.25em } h3 { font-size:1.15em } h4 { font-size:1.05em; font-weight: bold } h5 { font-size:1.05em } h6 { font-size:1.05em; font-weight: lighter } p { margin: .5em 0 0 0 } p.zs-meta-zettel { margin-top: .5em; margin-left: .5em } li,figure,figcaption,dl { margin: 0 } dt { margin: .5em 0 0 0 } dt+dd { margin-top: 0 } dd { margin: .5em 0 0 2em } dd > p:first-child { margin: 0 0 0 0 } blockquote { border-left: .5em solid lightgray; padding-left: 1em; margin-left: 1em; margin-right: 2em; } blockquote p { margin-bottom: .5em } table { border-collapse: collapse; border-spacing: 0; max-width: 100%; } td, th {text-align: left; padding: .25em .5em;} th { font-weight: bold } thead th { border-bottom: 2px solid hsl(0, 0%, 70%) } td { border-bottom: 1px solid hsl(0, 0%, 85%) } main form { padding: 0 .5em; margin: .5em 0 0 0; } main form:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } main form div { margin: .5em 0 0 0 } input { font-family: monospace } input[type="submit"],button,select { font: inherit } label { font-family: sans-serif; font-size:.9rem } textarea { font-family: monospace; resize: vertical; width: 100%; } .zs-input { padding: .5em; display:block; border:none; border-bottom:1px solid #ccc; width:100%; } input.zs-primary { float:right } input.zs-secondary { float:left } input.zs-upload { padding-left: 1em; padding-right: 1em; } a:not([class]) { text-decoration-skip-ink: auto } a.broken { text-decoration: line-through } a[rel~="external"]::after { content: "➚"; display: inline-block } img { max-width: 100% } img.right { float: right } ol.zs-endnotes { padding-top: .5em; border-top: 1px solid; } kbd { font-family:monospace } code,pre { font-family: monospace; font-size: 85%; } code { padding: .1em .2em; background: #f0f0f0; border: 1px solid #ccc; border-radius: .25em; } pre { padding: .5em .7em; max-width: 100%; overflow: auto; border: 1px solid #ccc; border-radius: .5em; background: #f0f0f0; } pre code { font-size: 95%; position: relative; padding: 0; border: none; } div.zs-indication { padding: .5em .7em; max-width: 100%; border-radius: .5em; border: 1px solid black; } div.zs-indication p:first-child { margin-top: 0 } span.zs-indication { border: 1px solid black; border-radius: .25em; padding: .1rem .2em; font-size: 95%; } .zs-info { background-color: lightblue; padding: .5em 1em; } .zs-warning { background-color: lightyellow; padding: .5em 1em; } .zs-error { background-color: lightpink; border-style: none !important; font-weight: bold; } td.left, th.left { text-align:left } td.center, th.center { text-align:center } td.right, th.right { text-align:right } .zs-font-size-0 { font-size:75% } .zs-font-size-1 { font-size:83% } .zs-font-size-2 { font-size:100% } .zs-font-size-3 { font-size:117% } .zs-font-size-4 { font-size:150% } .zs-font-size-5 { font-size:200% } .zs-deprecated { border-style: dashed; padding: .2em } .zs-meta { font-size:.75rem; color:#444; margin-bottom:1em; } .zs-meta a { color:#444 } h1+.zs-meta { margin-top:-1em } nav > details { margin-top:1em } details > summary { width: 100%; background-color: #eee; font-family:sans-serif; } details > ul { margin-top:0; padding-left:2em; background-color: #eee; } footer { padding: 0 1em } @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } |
Added internal/box/constbox/base.sxn.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | ;;;---------------------------------------------------------------------------- ;;; Copyright (c) 2023-present Detlef Stern ;;; ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. ;;; ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(@@@@ (html ,@(if lang `((@ (lang ,lang)))) (head (meta (@ (charset "utf-8"))) (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0"))) (meta (@ (name "generator") (content "Zettelstore"))) (meta (@ (name "format-detection") (content "telephone=no"))) ,@META-HEADER (link (@ (rel "stylesheet") (href ,css-base-url))) (link (@ (rel "stylesheet") (href ,css-user-url))) ,@(ROLE-DEFAULT-meta (current-binding)) (title ,title)) (body (nav (@ (class "zs-menu")) (a (@ (href ,home-url)) "Home") ,@(if with-auth `((div (@ (class "zs-dropdown")) (button "User") (nav (@ (class "zs-dropdown-content")) ,@(if user-is-valid `((a (@ (href ,user-zettel-url)) ,user-ident) (a (@ (href ,logout-url)) "Logout")) `((a (@ (href ,login-url)) "Login")) ) ))) ) (div (@ (class "zs-dropdown")) (button "Lists") (nav (@ (class "zs-dropdown-content")) ,@list-urls ,@(if (bound? 'refresh-url) `((a (@ (href ,refresh-url)) "Refresh"))) )) ,@(if new-zettel-links `((div (@ (class "zs-dropdown")) (button "New") (nav (@ (class "zs-dropdown-content")) ,@(map wui-link new-zettel-links) ))) ) (search (form (@ (action ,search-url)) (input (@ (type "search") (inputmode "search") (name ,query-key-query) (title "General search field, with same behaviour as search field in search result list") (placeholder "Search..") (dir "auto"))))) ) (main (@ (class "content")) ,DETAIL) ,@(if FOOTER `((footer (hr) ,@FOOTER))) ,@(if debug-mode '((div (b "WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!")))) ))) |
Added internal/box/constbox/constbox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package constbox puts zettel inside the executable. package constbox import ( "context" _ "embed" // Allow to embed file content "net/url" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) func init() { manager.Register( " const", func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &constBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "const").Int("boxnum", int64(cdata.Number)).Child(), number: cdata.Number, zettel: constZettelMap, enricher: cdata.Enricher, }, nil }) } type constHeader map[string]string type constZettel struct { header constHeader content zettel.Content } type constBox struct { log *logger.Logger number int zettel map[id.Zid]constZettel enricher box.Enricher } func (*constBox) Location() string { return "const:" } func (cb *constBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { if z, ok := cb.zettel[zid]; ok { cb.log.Trace().Msg("GetZettel") return zettel.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil } err := box.ErrZettelNotFound{Zid: zid} cb.log.Trace().Err(err).Msg("GetZettel/Err") return zettel.Zettel{}, err } func (cb *constBox) HasZettel(_ context.Context, zid id.Zid) bool { _, found := cb.zettel[zid] return found } func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyZid") for zid := range cb.zettel { if constraint(zid) { handle(zid) } } return nil } func (cb *constBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyMeta") for zid, zettel := range cb.zettel { if constraint(zid) { m := meta.NewWithData(zid, zettel.header) cb.enricher.Enrich(ctx, m, cb.number) handle(m) } } return nil } func (*constBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { if _, ok := cb.zettel[zid]; ok { err = box.ErrReadOnly } else { err = box.ErrZettelNotFound{Zid: zid} } cb.log.Trace().Err(err).Msg("DeleteZettel") return err } func (cb *constBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(cb.zettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } var constZettelMap = map[id.Zid]constZettel{ id.ZidConfiguration: { constHeader{ meta.KeyTitle: "Zettelstore Runtime Configuration", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxNone, meta.KeyCreated: "20200804111624", meta.KeyVisibility: meta.ValueVisibilityOwner, }, zettel.NewContent(nil)}, id.ZidLicense: { constHeader{ meta.KeyTitle: "Zettelstore License", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxText, meta.KeyCreated: "20210504135842", meta.KeyLang: meta.ValueLangEN, meta.KeyModified: "20220131153422", meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityPublic, }, zettel.NewContent(contentLicense)}, id.ZidAuthors: { constHeader{ meta.KeyTitle: "Zettelstore Contributors", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20210504135842", meta.KeyLang: meta.ValueLangEN, meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityLogin, }, zettel.NewContent(contentContributors)}, id.ZidDependencies: { constHeader{ meta.KeyTitle: "Zettelstore Dependencies", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityPublic, meta.KeyCreated: "20210504135842", meta.KeyModified: "20250312154700", }, zettel.NewContent(contentDependencies)}, id.ZidBaseTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Base HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230510155100", meta.KeyModified: "20241227212000", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentBaseSxn)}, id.ZidLoginTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Login Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", meta.KeyModified: "20240219145200", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentLoginSxn)}, id.ZidZettelTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Zettel HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230510155300", meta.KeyModified: "20250214153100", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentZettelSxn)}, id.ZidInfoTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Info HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", meta.KeyModified: "20241127170500", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentInfoSxn)}, id.ZidFormTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", meta.KeyModified: "20240219145200", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentFormSxn)}, id.ZidDeleteTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Delete HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", meta.KeyModified: "20241127170530", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentDeleteSxn)}, id.ZidListTemplate: { constHeader{ meta.KeyTitle: "Zettelstore List Zettel HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230704122100", meta.KeyModified: "20240219145200", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentListZettelSxn)}, id.ZidErrorTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Error HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20210305133215", meta.KeyModified: "20240219145200", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentErrorSxn)}, id.ZidSxnStart: { constHeader{ meta.KeyTitle: "Zettelstore Sxn Start Code", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230824160700", meta.KeyModified: "20240219145200", meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeyPrecursor: id.ZidSxnBase.String(), }, zettel.NewContent(contentStartCodeSxn)}, id.ZidSxnBase: { constHeader{ meta.KeyTitle: "Zettelstore Sxn Base Code", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230619132800", meta.KeyModified: "20241118173500", meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeyPrecursor: id.ZidSxnPrelude.String(), }, zettel.NewContent(contentBaseCodeSxn)}, id.ZidSxnPrelude: { constHeader{ meta.KeyTitle: "Zettelstore Sxn Prelude", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20231006181700", meta.KeyModified: "20240222121200", meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentPreludeSxn)}, id.ZidBaseCSS: { constHeader{ meta.KeyTitle: "Zettelstore Base CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxCSS, meta.KeyCreated: "20200804111624", meta.KeyModified: "20240827143500", meta.KeyVisibility: meta.ValueVisibilityPublic, }, zettel.NewContent(contentBaseCSS)}, id.ZidUserCSS: { constHeader{ meta.KeyTitle: "Zettelstore User CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxCSS, meta.KeyCreated: "20210622110143", meta.KeyVisibility: meta.ValueVisibilityPublic, }, zettel.NewContent([]byte("/* User-defined CSS */"))}, id.ZidEmoji: { constHeader{ meta.KeyTitle: "Zettelstore Generic Emoji", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxGif, meta.KeyReadOnly: meta.ValueTrue, meta.KeyCreated: "20210504175807", meta.KeyVisibility: meta.ValueVisibilityPublic, }, zettel.NewContent(contentEmoji)}, id.ZidTOCListsMenu: { constHeader{ meta.KeyTitle: "Lists Menu", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyCreated: "20241223205400", meta.KeyVisibility: meta.ValueVisibilityPublic, }, zettel.NewContent(contentMenuListsZettel)}, id.ZidTOCNewTemplate: { constHeader{ meta.KeyTitle: "New Menu", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyCreated: "20210217161829", meta.KeyModified: "20231129111800", meta.KeyVisibility: meta.ValueVisibilityCreator, }, zettel.NewContent(contentMenuNewZettel)}, id.ZidTemplateNewZettel: { constHeader{ meta.KeyTitle: "New Zettel", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20201028185209", meta.KeyModified: "20230929132900", meta.NewPrefix + meta.KeyRole: meta.ValueRoleZettel, meta.KeyVisibility: meta.ValueVisibilityCreator, }, zettel.NewContent(nil)}, id.ZidTemplateNewRole: { constHeader{ meta.KeyTitle: "New Role", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20231129110800", meta.NewPrefix + meta.KeyRole: meta.ValueRoleRole, meta.NewPrefix + meta.KeyTitle: "", meta.KeyVisibility: meta.ValueVisibilityCreator, }, zettel.NewContent(nil)}, id.ZidTemplateNewTag: { constHeader{ meta.KeyTitle: "New Tag", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20230929132400", meta.NewPrefix + meta.KeyRole: meta.ValueRoleTag, meta.NewPrefix + meta.KeyTitle: "#", meta.KeyVisibility: meta.ValueVisibilityCreator, }, zettel.NewContent(nil)}, id.ZidTemplateNewUser: { constHeader{ meta.KeyTitle: "New User", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxNone, meta.KeyCreated: "20201028185209", meta.NewPrefix + meta.KeyCredential: "", meta.NewPrefix + meta.KeyUserID: "", meta.NewPrefix + meta.KeyUserRole: meta.ValueUserRoleReader, meta.KeyVisibility: meta.ValueVisibilityOwner, }, zettel.NewContent(nil)}, id.ZidRoleZettelZettel: { constHeader{ meta.KeyTitle: meta.ValueRoleZettel, meta.KeyRole: meta.ValueRoleRole, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20231129161400", meta.KeyLang: meta.ValueLangEN, meta.KeyVisibility: meta.ValueVisibilityLogin, }, zettel.NewContent(contentRoleZettel)}, id.ZidRoleConfigurationZettel: { constHeader{ meta.KeyTitle: meta.ValueRoleConfiguration, meta.KeyRole: meta.ValueRoleRole, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20241213103100", meta.KeyLang: meta.ValueLangEN, meta.KeyVisibility: meta.ValueVisibilityLogin, }, zettel.NewContent(contentRoleConfiguration)}, id.ZidRoleRoleZettel: { constHeader{ meta.KeyTitle: meta.ValueRoleRole, meta.KeyRole: meta.ValueRoleRole, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20231129162900", meta.KeyLang: meta.ValueLangEN, meta.KeyVisibility: meta.ValueVisibilityLogin, }, zettel.NewContent(contentRoleRole)}, id.ZidRoleTagZettel: { constHeader{ meta.KeyTitle: meta.ValueRoleTag, meta.KeyRole: meta.ValueRoleRole, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20231129162000", meta.KeyLang: meta.ValueLangEN, meta.KeyVisibility: meta.ValueVisibilityLogin, }, zettel.NewContent(contentRoleTag)}, id.ZidAppDirectory: { constHeader{ meta.KeyTitle: "Zettelstore Application Directory", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxNone, meta.KeyLang: meta.ValueLangEN, meta.KeyCreated: "20240703235900", meta.KeyVisibility: meta.ValueVisibilityLogin, }, zettel.NewContent(nil)}, id.ZidDefaultHome: { constHeader{ meta.KeyTitle: "Home", meta.KeyRole: meta.ValueRoleZettel, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyCreated: "20210210190757", meta.KeyModified: "20241216105800", }, zettel.NewContent(contentHomeZettel)}, } //go:embed license.txt var contentLicense []byte //go:embed contributors.zettel var contentContributors []byte //go:embed dependencies.zettel var contentDependencies []byte //go:embed base.sxn var contentBaseSxn []byte //go:embed login.sxn var contentLoginSxn []byte //go:embed zettel.sxn var contentZettelSxn []byte //go:embed info.sxn var contentInfoSxn []byte //go:embed form.sxn var contentFormSxn []byte //go:embed delete.sxn var contentDeleteSxn []byte //go:embed listzettel.sxn var contentListZettelSxn []byte //go:embed error.sxn var contentErrorSxn []byte //go:embed start.sxn var contentStartCodeSxn []byte //go:embed wuicode.sxn var contentBaseCodeSxn []byte //go:embed prelude.sxn var contentPreludeSxn []byte //go:embed base.css var contentBaseCSS []byte //go:embed emoji_spin.gif var contentEmoji []byte //go:embed menu_lists.zettel var contentMenuListsZettel []byte //go:embed menu_new.zettel var contentMenuNewZettel []byte //go:embed rolezettel.zettel var contentRoleZettel []byte //go:embed roleconfiguration.zettel var contentRoleConfiguration []byte //go:embed rolerole.zettel var contentRoleRole []byte //go:embed roletag.zettel var contentRoleTag []byte //go:embed home.zettel var contentHomeZettel []byte |
Added internal/box/constbox/contributors.zettel.
> > > > > > > > | 1 2 3 4 5 6 7 8 | Zettelstore is a software for humans made from humans. === Licensor(s) * Detlef Stern [[mailto:ds@zettelstore.de]] ** Main author ** Maintainer === Contributors |
Added internal/box/constbox/delete.sxn.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | ;;;---------------------------------------------------------------------------- ;;; Copyright (c) 2023-present Detlef Stern ;;; ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. ;;; ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 "Delete Zettel " ,zid)) (p "Do you really want to delete this zettel?") ,@(if shadowed-box `((div (@ (class "zs-info")) (h2 "Information") (p "If you delete this zettel, the previously shadowed zettel from overlayed box " ,shadowed-box " becomes available.") )) ) ,@(if incoming `((div (@ (class "zs-warning")) (h2 "Warning!") (p "If you delete this zettel, incoming references from the following zettel will become invalid.") (ul ,@(map wui-item-link incoming)) )) ) ,@(if (and (bound? 'useless) useless) `((div (@ (class "zs-warning")) (h2 "Warning!") (p "Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.") (ul ,@(map wui-item useless)) )) ) ,(wui-meta-desc metapairs) (form (@ (method "POST")) (input (@ (class "zs-primary") (type "submit") (value "Delete")))) ) |
Added internal/box/constbox/dependencies.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | Zettelstore is made with the help of other software and other artifacts. Thank you very much! This zettel lists all of them, together with their licenses. === Go runtime and associated libraries ; License : BSD 3-Clause "New" or "Revised" License ``` Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` === Fsnotify ; URL : [[https://fsnotify.org/]] ; License : BSD 3-Clause "New" or "Revised" License ; Source : [[https://github.com/fsnotify/fsnotify]] ``` Copyright © 2012 The Go Authors. All rights reserved. Copyright © fsnotify Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` === yuin/goldmark ; URL & Source : [[https://github.com/yuin/goldmark]] ; License : MIT License ``` MIT License Copyright (c) 2019 Yusuke Inuzuka Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` === ASCIIToSVG ; URL : [[https://github.com/asciitosvg/asciitosvg]] ; License : MIT ; Remarks : ASCIIToSVG was incorporated into the source code of Zettelstore and later moved into package ''t73f.de/r/webs/aasvg''. The source code was changed substantially to adapt it to the needs of Zettelstore. ``` Copyright (c) 2015 The ASCIIToSVG Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` === Sx, SxWebs, Webs, Zero, Zettelstore-Client These are companion projects, written by the main developer of Zettelstore. They are published under the same license, [[EUPL v1.2, or later|00000000000004]]. ; URL & Source Sx : [[https://t73f.de/r/sx]] ; URL & Source SxWebs : [[https://t73f.de/r/sxwebs]] ; URL & Source Webs : [[https://t73f.de/r/webs]] ; URL & Source Zero : [[https://t73f.de/r/zero]] ; URL & Source Zettelstore-Client : [[https://t73f.de/r/zsc]] ; License: : European Union Public License, version 1.2 (EUPL v1.2), or later. |
Added internal/box/constbox/emoji_spin.gif.
cannot compute difference between binary files
Added internal/box/constbox/error.sxn.
> > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | ;;;---------------------------------------------------------------------------- ;;; Copyright (c) 2023-present Detlef Stern ;;; ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. ;;; ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 ,heading)) ,message ) |
Added internal/box/constbox/form.sxn.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | ;;;---------------------------------------------------------------------------- ;;; Copyright (c) 2023-present Detlef Stern ;;; ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. ;;; ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 ,heading)) (form (@ (action ,form-action-url) (method "POST") (enctype "multipart/form-data")) (div (label (@ (for "zs-title")) "Title " (a (@ (title "Main heading of this zettel.")) (@H "ⓘ"))) (input (@ (class "zs-input") (type "text") (id "zs-title") (name "title") (title "Title of this zettel") (placeholder "Title..") (value ,meta-title) (dir "auto") (autofocus)))) (div (label (@ (for "zs-role")) "Role " (a (@ (title "One word, without spaces, to set the main role of this zettel.")) (@H "ⓘ"))) (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-role") (name "role") (title "One word, letters and digits, but no spaces, to set the main role of the zettel.") (placeholder "role..") (value ,meta-role) (dir "auto") ,@(if role-data '((list "zs-role-data"))) )) ,@(wui-datalist "zs-role-data" role-data) ) (div (label (@ (for "zs-tags")) "Tags " (a (@ (title "Tags must begin with an '#' sign. They are separated by spaces.")) (@H "ⓘ"))) (input (@ (class "zs-input") (type "text") (id "zs-tags") (name "tags") (title "Tags/keywords to categorize the zettel. Each tags is a word that begins with a '#' character; they are separated by spaces") (placeholder "#tag") (value ,meta-tags) (dir "auto")))) (div (label (@ (for "zs-meta")) "Metadata " (a (@ (title "Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.")) (@H "ⓘ"))) (textarea (@ (class "zs-input") (id "zs-meta") (name "meta") (rows "4") (title "Additional metadata about the zettel") (placeholder "metakey: metavalue") (dir "auto")) ,meta)) (div (label (@ (for "zs-syntax")) "Syntax " (a (@ (title "Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).")) (@H "ⓘ"))) (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-syntax") (name "syntax") (title "Syntax/format of zettel content below, one word, letters and digits, no spaces.") (placeholder "syntax..") (value ,meta-syntax) (dir "auto") ,@(if syntax-data '((list "zs-syntax-data"))) )) ,@(wui-datalist "zs-syntax-data" syntax-data) ) ,@(if (bound? 'content) `((div (label (@ (for "zs-content")) "Content " (a (@ (title "Content for this zettel, according to above syntax.")) (@H "ⓘ"))) (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20") (title "Zettel content, according to the given syntax") (placeholder "Zettel content..") (dir "auto")) ,content) )) ) (div (input (@ (class "zs-primary") (type "submit") (value "Submit"))) (input (@ (class "zs-secondary") (type "submit") (value "Save") (formaction "?save"))) (input (@ (class "zs-upload") (type "file") (id "zs-file") (name "file"))) )) ) |
Added internal/box/constbox/home.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | === Thank you for using Zettelstore! You will find the latest information about Zettelstore at [[https://zettelstore.de]]. Check this website regularly for [[updates|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version. You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before updating. Sometimes, you have to edit some of your Zettelstore-related zettel before updating. Since Zettelstore is currently in a development state, every update might fix some of your problems. If you have problems concerning Zettelstore, do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. === Reporting errors If you have encountered an error, please include the content of the following zettel in your mail (if possible): * [[Zettelstore Version|00000000000001]]: {{00000000000001}} * [[Zettelstore Operating System|00000000000003]] * [[Zettelstore Startup Configuration|00000000000096]] * [[Zettelstore Runtime Configuration|00000000000100]] Additionally, you have to describe, what you did before that error occurs and what you expected instead. Please do not forget to include the error message, if there is one. Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"". Otherwise, only some zettel are linked. To enable expert mode, edit the zettel [[Zettelstore Runtime Configuration|00000000000100]]: please set the metadata value of the key ''expert-mode'' to true. To do so, enter the string ''expert-mode:true'' inside the edit view of the metadata. === Information about this zettel This zettel is your home zettel. It is part of the Zettelstore software itself. Every time you click on the [[Home|//]] link in the menu bar, you will be redirected to this zettel. You can change the content of this zettel by clicking on ""Edit"" above. This allows you to customize your home zettel. Alternatively, you can designate another zettel as your home zettel. Edit the [[Zettelstore Runtime Configuration|00000000000100]] by adding the metadata key ''home-zettel''. Its value is the identifier of the zettel that should act as the new home zettel. You will find the identifier of each zettel between their ""Edit"" and the ""Info"" link above. The identifier of this zettel is ''00010000000000''. If you provide a wrong identifier, this zettel will be shown as the home zettel. Take a look inside the manual for further details. |
Added internal/box/constbox/info.sxn.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | ;;;---------------------------------------------------------------------------- ;;; Copyright (c) 2023-present Detlef Stern ;;; ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. ;;; ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 "Information for Zettel " ,zid) (p (a (@ (href ,web-url)) "Web") (@H " · ") (a (@ (href ,context-url)) "Context") (@H " / ") (a (@ (href ,context-full-url)) "Full") ,@(if (bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit"))) ,@(ROLE-DEFAULT-actions (current-binding)) ,@(if (bound? 'reindex-url) `((@H " · ") (a (@ (href ,reindex-url)) "Reindex"))) ,@(if (bound? 'delete-url) `((@H " · ") (a (@ (href ,delete-url)) "Delete"))) ) ) (h2 "Interpreted Metadata") (table ,@(map wui-info-meta-table-row metadata)) (h2 "References") ,@(if local-links `((h3 "Local") (ul ,@(map wui-local-link local-links)))) ,@(if query-links `((h3 "Queries") (ul ,@(map wui-item-link query-links)))) ,@(if ext-links `((h3 "External") (ul ,@(map wui-item-popup-link ext-links)))) (h3 "Unlinked") ,@unlinked-content (form (label (@ (for "phrase")) "Search Phrase") (input (@ (class "zs-input") (type "text") (id "phrase") (name ,query-key-phrase) (placeholder "Phrase..") (value ,phrase))) ) (h2 "Parts and encodings") ,(wui-enc-matrix enc-eval) (h3 "Parsed (not evaluated)") ,(wui-enc-matrix enc-parsed) ,@(if shadow-links `((h2 "Shadowed Boxes") (ul ,@(map wui-item shadow-links)) ) ) ) |
Added internal/box/constbox/license.txt.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | Copyright (c) 2020-present Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the EU. The English version is included here. Please see https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official translations of the other languages. ------------------------------------------------------------------------------- EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016 This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such use is covered by a right of the copyright holder of the Work). The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following notice immediately following the copyright notice for the Work: Licensed under the EUPL or has expressed by any other means his willingness to license under the EUPL. 1. Definitions In this Licence, the following terms have the following meaning: — ‘The Licence’: this Licence. — ‘The Original Work’: the work or software distributed or communicated by the Licensor under this Licence, available as Source Code and also as Executable Code as the case may be. — ‘Derivative Works’: the works or software that could be created by the Licensee, based upon the Original Work or modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in the country mentioned in Article 15. — ‘The Work’: the Original Work or its Derivative Works. — ‘The Source Code’: the human-readable form of the Work which is the most convenient for people to study and modify. — ‘The Executable Code’: any code which has generally been compiled and which is meant to be interpreted by a computer as a program. — ‘The Licensor’: the natural or legal person that distributes or communicates the Work under the Licence. — ‘Contributor(s)’: any natural or legal person who modifies the Work under the Licence, or otherwise contributes to the creation of a Derivative Work. — ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of the Work under the terms of the Licence. — ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential functionalities at the disposal of any other natural or legal person. 2. Scope of the rights granted by the Licence The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for the duration of copyright vested in the Original Work: — use the Work in any circumstance and for all usage, — reproduce the Work, — modify the Work, and make Derivative Works based upon the Work, — communicate to the public, including the right to make available or display the Work or copies thereof to the public and perform publicly, as the case may be, the Work, — distribute the Work or copies thereof, — lend and rent the Work or copies thereof, — sublicense rights in the Work or copies thereof. Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the applicable law permits so. In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed by law in order to make effective the licence of the economic rights here above listed. The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the extent necessary to make use of the rights granted on the Work under this Licence. 3. Communication of the Source Code The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to distribute or communicate the Work. 4. Limitations on copyright Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations thereto. 5. Obligations of the Licensee The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those obligations are the following: Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work to carry prominent notices stating that the Work has been modified and the date of modification. Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless the Original Work is expressly distributed only under this version of the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the Work or Derivative Work that alter or restrict the terms of the Licence. Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available for as long as the Licensee continues to distribute or communicate the Work. Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the copyright notice. 6. Chain of Authorship The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions to the Work, under the terms of this Licence. 7. Disclaimer of Warranty The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work and may therefore contain defects or ‘bugs’ inherent to this type of development. For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this Licence. This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work. 8. Disclaimer of Liability Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, the Licensor will be liable under statutory product liability laws as far such laws apply to the Work. 9. Additional agreements While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by the fact You have accepted any warranty or additional liability. 10. Acceptance of the Licence The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms and conditions. Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution or Communication by You of the Work or copies thereof. 11. Information to the public In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, by offering to download the Work from a remote location) the distribution channel or media (for example, a website) must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence and the way it may be accessible, concluded, stored and reproduced by the Licensee. 12. Termination of the Licence The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms of the Licence. Such a termination will not terminate the licences of any person who has received the Work from the Licensee under the Licence, provided such persons remain in full compliance with the Licence. 13. Miscellaneous Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the Work. If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid and enforceable. The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. New versions of the Licence will be published with a unique version number. All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take advantage of the linguistic version of their choice. 14. Jurisdiction Without prejudice to specific agreement between parties, — any litigation resulting from the interpretation of this License, arising between the European Union institutions, bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, — any litigation arising between other parties and resulting from the interpretation of this License, will be subject to the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. 15. Applicable Law Without prejudice to specific agreement between parties, — this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, resides or has his registered office, — this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside a European Union Member State. Appendix ‘Compatible Licences’ according to Article 5 EUPL are: — GNU General Public License (GPL) v. 2, v. 3 — GNU Affero General Public License (AGPL) v. 3 — Open Software License (OSL) v. 2.1, v. 3.0 — Eclipse Public License (EPL) v. 1.0 — CeCILL v. 2.0, v. 2.1 — Mozilla Public Licence (MPL) v. 2 — GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 — Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software — European Union Public Licence (EUPL) v. 1.1, v. 1.2 — Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+) The European Commission may update this Appendix to later versions of the above licences without producing a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the covered Source Code from exclusive appropriation. All other changes or additions to this Appendix require the production of a new EUPL version. |
Added internal/box/constbox/listzettel.sxn.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | ;;;---------------------------------------------------------------------------- ;;; Copyright (c) 2023-present Detlef Stern ;;; ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. ;;; ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 ,heading)) (search (form (@ (action ,search-url)) (input (@ (class "zs-input") (type "search") (inputmode "search") (name ,query-key-query) (title "Contains the search that leads to the list below. You're allowed to modify it") (placeholder "Search..") (value ,query-value) (dir "auto"))))) ,@(if (bound? 'tag-zettel) `((p (@ (class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel)) ) ,@(if (bound? 'create-tag-zettel) `((p (@ (class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel)) ) ,@(if (bound? 'role-zettel) `((p (@ (class "zs-meta-zettel")) "Role zettel: " ,@role-zettel)) ) ,@(if (bound? 'create-role-zettel) `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel)) ) ,@content ,@endnotes (form (@ (action ,(if (bound? 'create-url) create-url))) ,(if (bound? 'data-url) `(@L "Other encodings" ,(if (> num-entries 3) `(@L " of these " ,num-entries " entries: ") ": ") (a (@ (href ,data-url)) "data") ", " (a (@ (href ,plain-url)) "plain") ) ) ,@(if (bound? 'create-url) `((input (@ (type "hidden") (name ,query-key-query) (value ,query-value))) (input (@ (type "hidden") (name ,query-key-seed) (value ,seed))) (input (@ (class "zs-primary") (type "submit") (value "Save As Zettel"))) ) ) ) ) |
Added internal/box/constbox/login.sxn.
> > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | ;;;---------------------------------------------------------------------------- ;;; Copyright (c) 2023-present Detlef Stern ;;; ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. ;;; ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 "Login")) ,@(if retry '((div (@ (class "zs-indication zs-error")) "Wrong user name / password. Try again."))) (form (@ (method "POST") (action "")) (div (label (@ (for "username")) "User name:") (input (@ (class "zs-input") (type "text") (id "username") (name "username") (placeholder "Your user name..") (autofocus)))) (div (label (@ (for "password")) "Password:") (input (@ (class "zs-input") (type "password") (id "password") (name "password") (placeholder "Your password..")))) (div (input (@ (class "zs-primary") (type "submit") (value "Login")))) ) ) |
Added internal/box/constbox/menu_lists.zettel.
> > > > > > > | 1 2 3 4 5 6 7 | This zettel lists all entries of the ""Lists"" menu. * [[List Zettel|query:]] * [[List Roles|query:|role]] * [[List Tags|query:|tags]] An additional ""Refresh"" menu item is automatically added if appropriate. |
Added internal/box/constbox/menu_new.zettel.
> > > > > > | 1 2 3 4 5 6 | This zettel lists all zettel that should act as a template for new zettel. These zettel will be included in the ""New"" menu of the WebUI. * [[New Zettel|00000000090001]] * [[New Role|00000000090004]] * [[New Tag|00000000090003]] * [[New User|00000000090002]] |
Added internal/box/constbox/prelude.sxn.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | ;;;---------------------------------------------------------------------------- ;;; Copyright (c) 2023-present Detlef Stern ;;; ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. ;;; ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- ;;; This zettel contains sxn definitions that are independent of specific ;;; subsystems, such as WebUI, API, or other. It just contains generic code to ;;; be used in all places. It asumes that the symbols NIL and T are defined. ;; not macro (defmacro not (x) `(if ,x NIL T)) ;; not= macro, to negate an equivalence (defmacro not= args `(not (= ,@args))) ;; let* macro ;; ;; (let* (BINDING ...) EXPR ...), where SYMBOL may occur in later bindings. (defmacro let* (bindings . body) (if (null? bindings) `(begin ,@body) `(let ((,(caar bindings) ,(cadar bindings))) (let* ,(cdr bindings) ,@body)))) ;; cond macro ;; ;; (cond ((COND EXPR) ...)) (defmacro cond clauses (if (null? clauses) () (let* ((clause (car clauses)) (the-cond (car clause))) (if (= the-cond T) `(begin ,@(cdr clause)) `(if ,the-cond (begin ,@(cdr clause)) (cond ,@(cdr clauses))))))) ;; and macro ;; ;; (and EXPR ...) (defmacro and args (cond ((null? args) T) ((null? (cdr args)) (car args)) (T `(if ,(car args) (and ,@(cdr args)))))) ;; or macro ;; ;; (or EXPR ...) (defmacro or args (cond ((null? args) NIL) ((null? (cdr args)) (car args)) (T `(if ,(car args) T (or ,@(cdr args)))))) |
Added internal/box/constbox/roleconfiguration.zettel.
> > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | Zettel with role ""configuration"" are used within Zettelstore to manage and to show the current configuration of the software. Typically, there are some public zettel that show the license of this software, its dependencies. There is some CSS code to make the default web user interface a litte bit nicer. The default image to signal a broken image can be configured too. Other zettel are only visible if an user has authenticated itself, or if there is no authentication enabled. In this case, one additional configuration zettel is the zettel containing the version number of this software. Other zettel are showing the supported metadata keys and supported syntax values. Zettel that allow to configure the menu of template to create new zettel are also using the role ""configuration"". Most important is the zettel that contains the runtime configuration. You may change its metadata value to change the behaviour of the software. One configuration is the ""expert mode"". If enabled, and if you are authorized to see them, you will discover some more zettel. For example, HTML templates to customize the default web user interface, to show the application log, to see statistics about zettel boxes, to show the host name and it operating system, and many more. You are allowed to add your own configuration zettel, for example if you want to customize the look and feel of zettel by placing relevant data into your own zettel. By default, user zettel (for authentification) use also the role ""configuration"". However, you are allowed to change this. |
Added internal/box/constbox/rolerole.zettel.
> > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 | A zettel with the role ""role"" describes a specific role. The described role must be the title of such a zettel. This zettel is such a zettel, as it describes the meaning of the role ""role"". Therefore it has the title ""role"" too. If you like, this zettel is a meta-role. You are free to create your own role-describing zettel. For example, you want to document the intended meaning of the role. You might also be interested to describe needed metadata so that some software is enabled to analyse or to process your zettel. |
Added internal/box/constbox/roletag.zettel.
> > > > > > | 1 2 3 4 5 6 | A zettel with role ""tag"" is a zettel that describes specific tag. The tag name must be the title of such a zettel. Such zettel are similar to this specific zettel: this zettel describes zettel with a role ""tag"". These zettel with the role ""tag"" describe specific tags. These might form a hierarchy of meta-tags (and meta-roles). |
Added internal/box/constbox/rolezettel.zettel.
> > > > > > > | 1 2 3 4 5 6 7 | A zettel with the role ""zettel"" is typically used to document your own thoughts. Such zettel are the main reason to use the software Zettelstore. The only predefined zettel with the role ""zettel"" is the [[default home zettel|00010000000000]], which contains some welcome information. You are free to change this. In this case you should modify this zettel too, so that it reflects your own use of zettel with the role ""zettel"". |
Added internal/box/constbox/start.sxn.
> > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | ;;;---------------------------------------------------------------------------- ;;; Copyright (c) 2023-present Detlef Stern ;;; ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. ;;; ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- ;;; This zettel is the start of the loading sequence for Sx code used in the ;;; Zettelstore. Via the precursor metadata, dependend zettel are evaluated ;;; before this zettel. You must always depend, directly or indirectly on the ;;; "Zettelstore Sxn Base Code" zettel. It provides the base definitions. |
Added internal/box/constbox/wuicode.sxn.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | ;;;---------------------------------------------------------------------------- ;;; Copyright (c) 2023-present Detlef Stern ;;; ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. ;;; ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- ;; Contains WebUI specific code, but not related to a specific template. ;; wui-list-item returns the argument as a HTML list item. (defun wui-item (s) `(li ,s)) ;; wui-info-meta-table-row takes a pair and translates it into a HTML table row ;; with two columns. (defun wui-info-meta-table-row (p) `(tr (td (@ (class zs-info-meta-key)) ,(car p)) (td (@ (class zs-info-meta-value)) ,(cdr p)))) ;; wui-local-link translates a local link into HTML. (defun wui-local-link (l) `(li (a (@ (href ,l )) ,l))) ;; wui-link takes a link (title . url) and returns a HTML reference. (defun wui-link (q) `(a (@ (href ,(cdr q))) ,(car q))) ;; wui-item-link taks a pair (text . url) and returns a HTML link inside ;; a list item. (defun wui-item-link (q) `(li ,(wui-link q))) ;; wui-tdata-link taks a pair (text . url) and returns a HTML link inside ;; a table data item. (defun wui-tdata-link (q) `(td ,(wui-link q))) ;; wui-item-popup-link is like 'wui-item-link, but the HTML link will open ;; a new tab / window. (defun wui-item-popup-link (e) `(li (a (@ (href ,e) (target "_blank") (rel "external noreferrer")) ,e))) ;; wui-option-value returns a value for an HTML option element. (defun wui-option-value (v) `(option (@ (value ,v)))) ;; wui-datalist returns a HTML datalist with the given HTML identifier and a ;; list of values. (defun wui-datalist (id lst) (if lst `((datalist (@ (id ,id)) ,@(map wui-option-value lst))))) ;; wui-pair-desc-item takes a pair '(term . text) and returns a list with ;; a HTML description term and a HTML description data. (defun wui-pair-desc-item (p) `((dt ,(car p)) (dd ,(cdr p)))) ;; wui-meta-desc returns a HTML description list made from the list of pairs ;; given. (defun wui-meta-desc (l) `(dl ,@(apply append (map wui-pair-desc-item l)))) ;; wui-enc-matrix returns the HTML table of all encodings and parts. (defun wui-enc-matrix (matrix) `(table ,@(map (lambda (row) `(tr (th ,(car row)) ,@(map wui-tdata-link (cdr row)))) matrix))) ;; CSS-ROLE-map is a mapping (pair list, assoc list) of role names to zettel ;; identifier. It is used in the base template to update the metadata of the ;; HTML page to include some role specific CSS code. ;; Referenced in function "ROLE-DEFAULT-meta". (defvar CSS-ROLE-map '()) ;; ROLE-DEFAULT-meta returns some metadata for the base template. Any role ;; specific code should include the returned list of this function. (defun ROLE-DEFAULT-meta (binding) `(,@(let* ((meta-role (binding-lookup 'meta-role binding)) (entry (assoc CSS-ROLE-map meta-role))) (if (pair? entry) `((link (@ (rel "stylesheet") (href ,(zid-content-path (cdr entry)))))) ) ) ) ) ;; ACTION-SEPARATOR defines a HTML value that separates actions links. (defvar ACTION-SEPARATOR '(@H " · ")) ;; ROLE-DEFAULT-actions returns the default text for actions. (defun ROLE-DEFAULT-actions (binding) `(,@(let ((copy-url (binding-lookup 'copy-url binding))) (if (defined? copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy")))) ,@(let ((version-url (binding-lookup 'version-url binding))) (if (defined? version-url) `((@H " · ") (a (@ (href ,version-url)) "Version")))) ,@(let ((sequel-url (binding-lookup 'sequel-url binding))) (if (defined? sequel-url) `((@H " · ") (a (@ (href ,sequel-url)) "Sequel")))) ,@(let ((folge-url (binding-lookup 'folge-url binding))) (if (defined? folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge")))) ) ) ;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag". (defun ROLE-tag-actions (binding) `(,@(ROLE-DEFAULT-actions binding) ,@(let ((title (binding-lookup 'title binding))) (if (and (defined? title) title) `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "tags:" title)))) "Zettel")) ) ) ) ) ;; ROLE-role-actions returns an additional action "Zettel" for zettel with role "role". (defun ROLE-role-actions (binding) `(,@(ROLE-DEFAULT-actions binding) ,@(let ((title (binding-lookup 'title binding))) (if (and (defined? title) title) `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "role:" title)))) "Zettel")) ) ) ) ) ;; ROLE-DEFAULT-heading returns the default text for headings, below the ;; references of a zettel. In most cases it should be called from an ;; overwriting function. (defun ROLE-DEFAULT-heading (binding) `(,@(let ((meta-url (binding-lookup 'meta-url binding))) (if (defined? meta-url) `((br) "URL: " ,(url-to-html meta-url)))) ,@(let ((urls (binding-lookup 'urls binding))) (if (defined? urls) (map (lambda (u) `(@L (br) ,(car u) ": " ,(url-to-html (cdr u)))) urls) ) ) ,@(let ((meta-author (binding-lookup 'meta-author binding))) (if (and (defined? meta-author) meta-author) `((br) "By " ,meta-author))) ) ) |
Added internal/box/constbox/zettel.sxn.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | ;;;---------------------------------------------------------------------------- ;;; Copyright (c) 2023-present Detlef Stern ;;; ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. ;;; ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 ,heading) (div (@ (class "zs-meta")) ,@(if (bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " · "))) ,zid (@H " · ") (a (@ (href ,info-url)) "Info") (@H " · ") "(" ,@(if (bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role))) ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role)) `((@H " → ") (a (@ (href ,folge-role-url)) ,meta-folge-role))) ")" ,@(if tag-refs `((@H " · ") ,@tag-refs)) ,@(ROLE-DEFAULT-actions (current-binding)) ,@(if superior-refs `((br) "Superior: " ,superior-refs)) ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs)) ,@(if prequel-refs `((br) "Prequel: " ,prequel-refs)) ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs)) ,@(ROLE-DEFAULT-heading (current-binding)) ) ) ,@content ,endnotes ,@(if (or folge-links sequel-links back-links successor-links subordinate-links) `((nav ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links))))) ,@(if sequel-links `((details (@ (,sequel-open)) (summary "Sequel") (ul ,@(map wui-item-link sequel-links))))) ,@(if successor-links `((details (@ (,successor-open)) (summary "Successors") (ul ,@(map wui-item-link successor-links))))) ,@(if subordinate-links `((details (@ (,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links))))) ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links))))) )) ) ) |
Added internal/box/dirbox/dirbox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package dirbox provides a directory-based zettel box. package dirbox import ( "context" "errors" "net/url" "os" "path/filepath" "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/box/notify" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) func init() { manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { var log *logger.Logger if krnl := kernel.Main; krnl != nil { log = krnl.GetLogger(kernel.BoxService).Clone().Str("box", "dir").Int("boxnum", int64(cdata.Number)).Child() } path := getDirPath(u) if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { return nil, err } dp := dirBox{ log: log, number: cdata.Number, location: u.String(), readonly: box.GetQueryBool(u, "readonly"), cdata: *cdata, dir: path, notifySpec: getDirSrvInfo(log, u.Query().Get("type")), fSrvs: makePrime(uint32(box.GetQueryInt(u, "worker", 1, 7, 1499))), } return &dp, nil }) } func makePrime(n uint32) uint32 { for !isPrime(n) { n++ } return n } func isPrime(n uint32) bool { if n == 0 { return false } if n <= 3 { return true } if n%2 == 0 { return false } for i := uint32(3); i*i <= n; i += 2 { if n%i == 0 { return false } } return true } type notifyTypeSpec int const ( _ notifyTypeSpec = iota dirNotifyAny dirNotifySimple dirNotifyFS ) func getDirSrvInfo(log *logger.Logger, notifyType string) notifyTypeSpec { for range 2 { switch notifyType { case kernel.BoxDirTypeNotify: return dirNotifyFS case kernel.BoxDirTypeSimple: return dirNotifySimple default: notifyType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string) } } log.Error().Str("notifyType", notifyType).Msg("Unable to set notify type, using a default") return dirNotifySimple } func getDirPath(u *url.URL) string { if u.Opaque != "" { return filepath.Clean(u.Opaque) } return filepath.Clean(u.Path) } // dirBox uses a directory to store zettel as files. type dirBox struct { log *logger.Logger number int location string readonly bool cdata manager.ConnectData dir string notifySpec notifyTypeSpec dirSrv *notify.DirService fSrvs uint32 fCmds []chan fileCmd mxCmds sync.RWMutex } func (dp *dirBox) Location() string { return dp.location } func (dp *dirBox) State() box.StartState { if ds := dp.dirSrv; ds != nil { switch ds.State() { case notify.DsCreated: return box.StartStateStopped case notify.DsStarting: return box.StartStateStarting case notify.DsWorking: return box.StartStateStarted case notify.DsMissing: return box.StartStateStarted case notify.DsStopping: return box.StartStateStopping } } return box.StartStateStopped } func (dp *dirBox) Start(context.Context) error { dp.mxCmds.Lock() defer dp.mxCmds.Unlock() dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) for i := range dp.fSrvs { cc := make(chan fileCmd) go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc) dp.fCmds = append(dp.fCmds, cc) } var notifier notify.Notifier var err error switch dp.notifySpec { case dirNotifySimple: notifier, err = notify.NewSimpleDirNotifier(dp.log.Clone().Str("notify", "simple").Child(), dp.dir) default: notifier, err = notify.NewFSDirNotifier(dp.log.Clone().Str("notify", "fs").Child(), dp.dir) } if err != nil { dp.log.Error().Err(err).Msg("Unable to create directory supervisor") dp.stopFileServices() return err } dp.dirSrv = notify.NewDirService( dp, dp.log.Clone().Str("sub", "dirsrv").Child(), notifier, dp.cdata.Notify, ) dp.dirSrv.Start() return nil } func (dp *dirBox) Refresh(_ context.Context) { dp.dirSrv.Refresh() dp.log.Trace().Msg("Refresh") } func (dp *dirBox) Stop(_ context.Context) { dirSrv := dp.dirSrv dp.dirSrv = nil if dirSrv != nil { dirSrv.Stop() } dp.stopFileServices() } func (dp *dirBox) stopFileServices() { for _, c := range dp.fCmds { close(c) } } func (dp *dirBox) notifyChanged(zid id.Zid, reason box.UpdateReason) { if notify := dp.cdata.Notify; notify != nil { dp.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChanged") notify(dp, zid, reason) } } func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd { // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function sum := 2166136261 ^ uint32(zid) sum *= 16777619 sum ^= uint32(zid >> 32) sum *= 16777619 dp.mxCmds.RLock() defer dp.mxCmds.RUnlock() return dp.fCmds[sum%dp.fSrvs] } func (dp *dirBox) CanCreateZettel(_ context.Context) bool { return !dp.readonly } func (dp *dirBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { if dp.readonly { return id.Invalid, box.ErrReadOnly } newZid, err := dp.dirSrv.SetNewDirEntry() if err != nil { return id.Invalid, err } meta := zettel.Meta meta.Zid = newZid entry := notify.DirEntry{Zid: newZid} dp.updateEntryFromMetaContent(&entry, meta, zettel.Content) err = dp.srvSetZettel(ctx, &entry, zettel) if err == nil { err = dp.dirSrv.UpdateDirEntry(&entry) } dp.notifyChanged(meta.Zid, box.OnZettel) dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel") return meta.Zid, err } func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } m, c, err := dp.srvGetMetaContent(ctx, entry, zid) if err != nil { return zettel.Zettel{}, err } zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(c)} dp.log.Trace().Zid(zid).Msg("GetZettel") return zettel, nil } func (dp *dirBox) HasZettel(_ context.Context, zid id.Zid) bool { return dp.dirSrv.GetDirEntry(zid).IsValid() } func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") for _, entry := range entries { handle(entry.Zid) } return nil } func (dp *dirBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta") // The following loop could be parallelized if needed for performance. for _, entry := range entries { m, err := dp.srvGetMeta(ctx, entry, entry.Zid) if err != nil { dp.log.Trace().Err(err).Msg("ApplyMeta/getMeta") return err } dp.cdata.Enricher.Enrich(ctx, m, dp.number) handle(m) } return nil } func (dp *dirBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { return !dp.readonly } func (dp *dirBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { if dp.readonly { return box.ErrReadOnly } meta := zettel.Meta zid := meta.Zid if !zid.IsValid() { return box.ErrInvalidZid{Zid: zid.String()} } entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { // Existing zettel, but new in this box. entry = ¬ify.DirEntry{Zid: zid} } dp.updateEntryFromMetaContent(entry, meta, zettel.Content) dp.dirSrv.UpdateDirEntry(entry) err := dp.srvSetZettel(ctx, entry, zettel) if err == nil { dp.notifyChanged(zid, box.OnZettel) } dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel") return err } func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content zettel.Content) { entry.SetupFromMetaContent(m, content, dp.cdata.Config.GetZettelFileSyntax) } func (dp *dirBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { if dp.readonly { return false } entry := dp.dirSrv.GetDirEntry(zid) return entry.IsValid() } func (dp *dirBox) DeleteZettel(ctx context.Context, zid id.Zid) error { if dp.readonly { return box.ErrReadOnly } entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return box.ErrZettelNotFound{Zid: zid} } err := dp.dirSrv.DeleteDirEntry(zid) if err != nil { return nil } err = dp.srvDeleteZettel(ctx, entry, zid) if err == nil { dp.notifyChanged(zid, box.OnDelete) } dp.log.Trace().Zid(zid).Err(err).Msg("DeleteZettel") return err } func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = dp.readonly st.Zettel = dp.dirSrv.NumDirEntries() dp.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } |
Added internal/box/dirbox/dirbox_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package dirbox import "testing" func TestIsPrime(t *testing.T) { testcases := []struct { n uint32 exp bool }{ {0, false}, {1, true}, {2, true}, {3, true}, {4, false}, {5, true}, {6, false}, {7, true}, {8, false}, {9, false}, {10, false}, {11, true}, {12, false}, {13, true}, {14, false}, {15, false}, {17, true}, {19, true}, {21, false}, {23, true}, {25, false}, {27, false}, {29, true}, {31, true}, {33, false}, {35, false}, } for _, tc := range testcases { got := isPrime(tc.n) if got != tc.exp { t.Errorf("isPrime(%d)=%v, but got %v", tc.n, tc.exp, got) } } } func TestMakePrime(t *testing.T) { for i := range uint32(1500) { np := makePrime(i) if np < i { t.Errorf("makePrime(%d) < %d", i, np) continue } if !isPrime(np) { t.Errorf("makePrime(%d) == %d is not prime", i, np) continue } if isPrime(i) && i != np { t.Errorf("%d is already prime, but got %d as next prime", i, np) continue } } } |
Added internal/box/dirbox/service.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package dirbox import ( "context" "fmt" "io" "os" "path/filepath" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/box/filebox" "zettelstore.de/z/internal/box/notify" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/zettel" ) func fileService(i uint32, log *logger.Logger, dirPath string, cmds <-chan fileCmd) { // Something may panic. Ensure a running service. defer func() { if ri := recover(); ri != nil { kernel.Main.LogRecover("FileService", ri) go fileService(i, log, dirPath, cmds) } }() log.Debug().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started") for cmd := range cmds { cmd.run(dirPath) } log.Debug().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service stopped") } type fileCmd interface { run(string) } const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing. // COMMAND: srvGetMeta ---------------------------------------- // // Retrieves the meta data from a zettel. func (dp *dirBox) srvGetMeta(ctx context.Context, entry *notify.DirEntry, zid id.Zid) (*meta.Meta, error) { rc := make(chan resGetMeta, 1) dp.getFileChan(zid) <- &fileGetMeta{entry, rc} ctx, cancel := context.WithTimeout(ctx, serviceTimeout) defer cancel() select { case res := <-rc: return res.meta, res.err case <-ctx.Done(): return nil, ctx.Err() } } type fileGetMeta struct { entry *notify.DirEntry rc chan<- resGetMeta } type resGetMeta struct { meta *meta.Meta err error } func (cmd *fileGetMeta) run(dirPath string) { var m *meta.Meta var err error entry := cmd.entry zid := entry.Zid if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt if contentName == "" || contentExt == "" { err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid) } else if entry.HasMetaInContent() { m, _, err = parseMetaContentFile(zid, filepath.Join(dirPath, contentName)) } else { m = filebox.CalcDefaultMeta(zid, contentExt) } } else { m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName)) } if err == nil { cmdCleanupMeta(m, entry) } cmd.rc <- resGetMeta{m, err} } // COMMAND: srvGetMetaContent ---------------------------------------- // // Retrieves the meta data and the content of a zettel. func (dp *dirBox) srvGetMetaContent(ctx context.Context, entry *notify.DirEntry, zid id.Zid) (*meta.Meta, []byte, error) { rc := make(chan resGetMetaContent, 1) dp.getFileChan(zid) <- &fileGetMetaContent{entry, rc} ctx, cancel := context.WithTimeout(ctx, serviceTimeout) defer cancel() select { case res := <-rc: return res.meta, res.content, res.err case <-ctx.Done(): return nil, nil, ctx.Err() } } type fileGetMetaContent struct { entry *notify.DirEntry rc chan<- resGetMetaContent } type resGetMetaContent struct { meta *meta.Meta content []byte err error } func (cmd *fileGetMetaContent) run(dirPath string) { var m *meta.Meta var content []byte var err error entry := cmd.entry zid := entry.Zid contentName := entry.ContentName contentExt := entry.ContentExt contentPath := filepath.Join(dirPath, contentName) if metaName := entry.MetaName; metaName == "" { if contentName == "" || contentExt == "" { err = fmt.Errorf("no meta, no content in getMetaContent, zid=%v", zid) } else if entry.HasMetaInContent() { m, content, err = parseMetaContentFile(zid, contentPath) } else { m = filebox.CalcDefaultMeta(zid, contentExt) content, err = os.ReadFile(contentPath) } } else { m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName)) if contentName != "" { var err1 error content, err1 = os.ReadFile(contentPath) if err == nil { err = err1 } } } if err == nil { cmdCleanupMeta(m, entry) } cmd.rc <- resGetMetaContent{m, content, err} } // COMMAND: srvSetZettel ---------------------------------------- // // Writes a new or exsting zettel. func (dp *dirBox) srvSetZettel(ctx context.Context, entry *notify.DirEntry, zettel zettel.Zettel) error { rc := make(chan resSetZettel, 1) dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc} ctx, cancel := context.WithTimeout(ctx, serviceTimeout) defer cancel() select { case err := <-rc: return err case <-ctx.Done(): return ctx.Err() } } type fileSetZettel struct { entry *notify.DirEntry zettel zettel.Zettel rc chan<- resSetZettel } type resSetZettel = error func (cmd *fileSetZettel) run(dirPath string) { var err error entry := cmd.entry zid := entry.Zid contentName := entry.ContentName m := cmd.zettel.Meta content := cmd.zettel.Content.AsBytes() metaName := entry.MetaName if metaName == "" { if contentName == "" { err = fmt.Errorf("no meta, no content in setZettel, zid=%v", zid) } else { contentPath := filepath.Join(dirPath, contentName) if entry.HasMetaInContent() { err = writeZettelFile(contentPath, m, content) cmd.rc <- err return } err = writeFileContent(contentPath, content) } cmd.rc <- err return } err = writeMetaFile(filepath.Join(dirPath, metaName), m) if err == nil && contentName != "" { err = writeFileContent(filepath.Join(dirPath, contentName), content) } cmd.rc <- err } func writeMetaFile(metaPath string, m *meta.Meta) error { metaFile, err := openFileWrite(metaPath) if err != nil { return err } err = writeFileZid(metaFile, m.Zid) if err == nil { _, err = m.WriteComputed(metaFile) } if err1 := metaFile.Close(); err == nil { err = err1 } return err } func writeZettelFile(contentPath string, m *meta.Meta, content []byte) error { zettelFile, err := openFileWrite(contentPath) if err != nil { return err } err = writeMetaHeader(zettelFile, m) if err == nil { _, err = zettelFile.Write(content) } if err1 := zettelFile.Close(); err == nil { err = err1 } return err } var ( newline = []byte{'\n'} yamlSep = []byte{'-', '-', '-', '\n'} ) func writeMetaHeader(w io.Writer, m *meta.Meta) (err error) { if m.YamlSep { _, err = w.Write(yamlSep) if err != nil { return err } } err = writeFileZid(w, m.Zid) if err != nil { return err } _, err = m.WriteComputed(w) if err != nil { return err } if m.YamlSep { _, err = w.Write(yamlSep) } else { _, err = w.Write(newline) } return err } // COMMAND: srvDeleteZettel ---------------------------------------- // // Deletes an existing zettel. func (dp *dirBox) srvDeleteZettel(ctx context.Context, entry *notify.DirEntry, zid id.Zid) error { rc := make(chan resDeleteZettel, 1) dp.getFileChan(zid) <- &fileDeleteZettel{entry, rc} ctx, cancel := context.WithTimeout(ctx, serviceTimeout) defer cancel() select { case err := <-rc: return err case <-ctx.Done(): return ctx.Err() } } type fileDeleteZettel struct { entry *notify.DirEntry rc chan<- resDeleteZettel } type resDeleteZettel = error func (cmd *fileDeleteZettel) run(dirPath string) { var err error entry := cmd.entry contentName := entry.ContentName contentPath := filepath.Join(dirPath, contentName) if metaName := entry.MetaName; metaName == "" { if contentName == "" { err = fmt.Errorf("no meta, no content in deleteZettel, zid=%v", entry.Zid) } else { err = os.Remove(contentPath) } } else { if contentName != "" { err = os.Remove(contentPath) } err1 := os.Remove(filepath.Join(dirPath, metaName)) if err == nil { err = err1 } } for _, dupName := range entry.UselessFiles { err1 := os.Remove(filepath.Join(dirPath, dupName)) if err == nil { err = err1 } } cmd.rc <- err } // Utility functions ---------------------------------------- func parseMetaFile(zid id.Zid, path string) (*meta.Meta, error) { src, err := os.ReadFile(path) if err != nil { return nil, err } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, []byte, error) { src, err := os.ReadFile(path) if err != nil { return nil, nil, err } inp := input.NewInput(src) meta := meta.NewFromInput(zid, inp) return meta, src[inp.Pos:], nil } func cmdCleanupMeta(m *meta.Meta, entry *notify.DirEntry) { filebox.CleanupMeta( m, entry.Zid, entry.ContentExt, entry.MetaName != "", entry.UselessFiles, ) } // fileMode to create a new file: user, group, and all are allowed to read and write. // // If you want to forbid others or the group to read or to write, you must set // umask(1) accordingly. const fileMode os.FileMode = 0666 // func openFileWrite(path string) (*os.File, error) { return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode) } func writeFileZid(w io.Writer, zid id.Zid) error { _, err := io.WriteString(w, "id: ") if err == nil { _, err = w.Write(zid.Bytes()) if err == nil { _, err = io.WriteString(w, "\n") } } return err } func writeFileContent(path string, content []byte) error { f, err := openFileWrite(path) if err == nil { _, err = f.Write(content) if err1 := f.Close(); err == nil { err = err1 } } return err } |
Added internal/box/filebox/filebox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package filebox provides boxes that are stored in a file. package filebox import ( "errors" "net/url" "path/filepath" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/kernel" ) func init() { manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { path := getFilepathFromURL(u) ext := strings.ToLower(filepath.Ext(path)) if ext != ".zip" { return nil, errors.New("unknown extension '" + ext + "' in box URL: " + u.String()) } return &zipBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "zip").Int("boxnum", int64(cdata.Number)).Child(), number: cdata.Number, name: path, enricher: cdata.Enricher, notify: cdata.Notify, }, nil }) } func getFilepathFromURL(u *url.URL) string { name := u.Opaque if name == "" { name = u.Path } components := strings.Split(name, "/") fileName := filepath.Join(components...) if len(components) > 0 && components[0] == "" { return "/" + fileName } return fileName } var alternativeSyntax = map[string]meta.Value{ "htm": "html", } func calculateSyntax(ext string) meta.Value { ext = strings.ToLower(ext) if syntax, ok := alternativeSyntax[ext]; ok { return syntax } return meta.Value(ext) } // CalcDefaultMeta returns metadata with default values for the given entry. func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta { m := meta.New(zid) m.Set(meta.KeySyntax, calculateSyntax(ext)) return m } // CleanupMeta enhances the given metadata. func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta bool, uselessFiles []string) { if inMeta { if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { dm := CalcDefaultMeta(zid, ext) syntax, ok = dm.Get(meta.KeySyntax) if !ok { panic("Default meta must contain syntax") } m.Set(meta.KeySyntax, syntax) } } if len(uselessFiles) > 0 { m.Set(meta.KeyUselessFiles, meta.Value(strings.Join(uselessFiles, " "))) } } |
Added internal/box/filebox/zipbox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package filebox import ( "archive/zip" "context" "fmt" "io" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/notify" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) type zipBox struct { log *logger.Logger number int name string enricher box.Enricher notify box.UpdateNotifier dirSrv *notify.DirService } func (zb *zipBox) Location() string { if strings.HasPrefix(zb.name, "/") { return "file://" + zb.name } return "file:" + zb.name } func (zb *zipBox) State() box.StartState { if ds := zb.dirSrv; ds != nil { switch ds.State() { case notify.DsCreated: return box.StartStateStopped case notify.DsStarting: return box.StartStateStarting case notify.DsWorking: return box.StartStateStarted case notify.DsMissing: return box.StartStateStarted case notify.DsStopping: return box.StartStateStopping } } return box.StartStateStopped } func (zb *zipBox) Start(context.Context) error { reader, err := zip.OpenReader(zb.name) if err != nil { return err } reader.Close() zipNotifier := notify.NewSimpleZipNotifier(zb.log, zb.name) zb.dirSrv = notify.NewDirService(zb, zb.log, zipNotifier, zb.notify) zb.dirSrv.Start() return nil } func (zb *zipBox) Refresh(_ context.Context) { zb.dirSrv.Refresh() zb.log.Trace().Msg("Refresh") } func (zb *zipBox) Stop(context.Context) { zb.dirSrv.Stop() zb.dirSrv = nil } func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } reader, err := zip.OpenReader(zb.name) if err != nil { return zettel.Zettel{}, err } defer reader.Close() var m *meta.Meta var src []byte var inMeta bool contentName := entry.ContentName if metaName := entry.MetaName; metaName == "" { if contentName == "" { err = fmt.Errorf("no meta, no content in getZettel, zid=%v", zid) return zettel.Zettel{}, err } src, err = readZipFileContent(reader, entry.ContentName) if err != nil { return zettel.Zettel{}, err } if entry.HasMetaInContent() { inp := input.NewInput(src) m = meta.NewFromInput(zid, inp) src = src[inp.Pos:] } else { m = CalcDefaultMeta(zid, entry.ContentExt) } } else { m, err = readZipMetaFile(reader, zid, metaName) if err != nil { return zettel.Zettel{}, err } inMeta = true if contentName != "" { src, err = readZipFileContent(reader, entry.ContentName) if err != nil { return zettel.Zettel{}, err } } } CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles) zb.log.Trace().Zid(zid).Msg("GetZettel") return zettel.Zettel{Meta: m, Content: zettel.NewContent(src)}, nil } func (zb *zipBox) HasZettel(_ context.Context, zid id.Zid) bool { return zb.dirSrv.GetDirEntry(zid).IsValid() } func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := zb.dirSrv.GetDirEntries(constraint) zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") for _, entry := range entries { handle(entry.Zid) } return nil } func (zb *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { reader, err := zip.OpenReader(zb.name) if err != nil { return err } defer reader.Close() entries := zb.dirSrv.GetDirEntries(constraint) zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta") for _, entry := range entries { if !constraint(entry.Zid) { continue } m, err2 := zb.readZipMeta(reader, entry.Zid, entry) if err2 != nil { continue } zb.enricher.Enrich(ctx, m, zb.number) handle(m) } return nil } func (*zipBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (zb *zipBox) DeleteZettel(_ context.Context, zid id.Zid) error { err := box.ErrReadOnly entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { err = box.ErrZettelNotFound{Zid: zid} } zb.log.Trace().Err(err).Msg("DeleteZettel") return err } func (zb *zipBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = zb.dirSrv.NumDirEntries() zb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } func (zb *zipBox) readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *notify.DirEntry) (m *meta.Meta, err error) { var inMeta bool if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt if contentName == "" || contentExt == "" { err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid) } else if entry.HasMetaInContent() { m, err = readZipMetaFile(reader, zid, contentName) } else { m = CalcDefaultMeta(zid, contentExt) } } else { m, err = readZipMetaFile(reader, zid, metaName) } if err == nil { CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles) } return m, err } func readZipMetaFile(reader *zip.ReadCloser, zid id.Zid, name string) (*meta.Meta, error) { src, err := readZipFileContent(reader, name) if err != nil { return nil, err } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } func readZipFileContent(reader *zip.ReadCloser, name string) ([]byte, error) { f, err := reader.Open(name) if err != nil { return nil, err } defer f.Close() return io.ReadAll(f) } |
Added internal/box/helper.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package box import ( "net/url" "strconv" "time" "t73f.de/r/zsc/domain/id" ) // GetNewZid calculates a new and unused zettel identifier, based on the current date and time. func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) { withSeconds := false for range 90 { // Must be completed within 9 seconds (less than web/server.writeTimeout) zid := id.New(withSeconds) found, err := testZid(zid) if err != nil { return id.Invalid, err } if found { return zid, nil } // TODO: do not wait here unconditionally. time.Sleep(100 * time.Millisecond) withSeconds = true } return id.Invalid, ErrConflict } // GetQueryBool is a helper function to extract bool values from a box URI. func GetQueryBool(u *url.URL, key string) bool { _, ok := u.Query()[key] return ok } // GetQueryInt is a helper function to extract int values of a specified range from a box URI. func GetQueryInt(u *url.URL, key string, minVal, defVal, maxVal int) int { sVal := u.Query().Get(key) if sVal == "" { return defVal } iVal, err := strconv.Atoi(sVal) if err != nil { return defVal } if iVal < minVal { return minVal } if iVal > maxVal { return maxVal } return iVal } |
Added internal/box/manager/anteroom.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" ) type arAction int const ( arNothing arAction = iota arReload arZettel ) type anteroom struct { next *anteroom waiting *idset.Set curLoad int reload bool } type anteroomQueue struct { mx sync.Mutex first *anteroom last *anteroom maxLoad int } func newAnteroomQueue(maxLoad int) *anteroomQueue { return &anteroomQueue{maxLoad: maxLoad} } func (ar *anteroomQueue) EnqueueZettel(zid id.Zid) { if !zid.IsValid() { return } ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { ar.first = ar.makeAnteroom(zid) ar.last = ar.first return } for room := ar.first; room != nil; room = room.next { if room.reload { continue // Do not put zettel in reload room } if room.waiting.Contains(zid) { // Zettel is already waiting. Nothing to do. return } } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { room.waiting.Add(zid) room.curLoad++ return } room := ar.makeAnteroom(zid) ar.last.next = room ar.last = room } func (ar *anteroomQueue) makeAnteroom(zid id.Zid) *anteroom { if zid == id.Invalid { panic(zid) } waiting := idset.NewCap(max(ar.maxLoad, 100), zid) return &anteroom{next: nil, waiting: waiting, curLoad: 1, reload: false} } func (ar *anteroomQueue) Reset() { ar.mx.Lock() defer ar.mx.Unlock() ar.first = &anteroom{next: nil, waiting: nil, curLoad: 0, reload: true} ar.last = ar.first } func (ar *anteroomQueue) Reload(allZids *idset.Set) { ar.mx.Lock() defer ar.mx.Unlock() ar.deleteReloadedRooms() if !allZids.IsEmpty() { ar.first = &anteroom{next: ar.first, waiting: allZids, curLoad: allZids.Length(), reload: true} if ar.first.next == nil { ar.last = ar.first } } else { ar.first = nil ar.last = nil } } func (ar *anteroomQueue) deleteReloadedRooms() { room := ar.first for room != nil && room.reload { room = room.next } ar.first = room if room == nil { ar.last = nil } } func (ar *anteroomQueue) Dequeue() (arAction, id.Zid, bool) { ar.mx.Lock() defer ar.mx.Unlock() first := ar.first if first != nil { if first.waiting == nil && first.reload { ar.removeFirst() return arReload, id.Invalid, false } if zid, found := first.waiting.Pop(); found { if first.waiting.IsEmpty() { ar.removeFirst() } return arZettel, zid, first.reload } ar.removeFirst() } return arNothing, id.Invalid, false } func (ar *anteroomQueue) removeFirst() { ar.first = ar.first.next if ar.first == nil { ar.last = nil } } |
Added internal/box/manager/anteroom_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "testing" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" ) func TestSimple(t *testing.T) { t.Parallel() ar := newAnteroomQueue(2) ar.EnqueueZettel(id.Zid(1)) action, zid, lastReload := ar.Dequeue() if zid != id.Zid(1) || action != arZettel || lastReload { t.Errorf("Expected arZettel/1/false, but got %v/%v/%v", action, zid, lastReload) } _, zid, _ = ar.Dequeue() if zid != id.Invalid { t.Errorf("Expected invalid Zid, but got %v", zid) } ar.EnqueueZettel(id.Zid(1)) ar.EnqueueZettel(id.Zid(2)) if ar.first != ar.last { t.Errorf("Expected one room, but got more") } ar.EnqueueZettel(id.Zid(3)) if ar.first == ar.last { t.Errorf("Expected more than one room, but got only one") } count := 0 for ; count < 1000; count++ { action, _, _ = ar.Dequeue() if action == arNothing { break } } if count != 3 { t.Errorf("Expected 3 dequeues, but got %v", count) } } func TestReset(t *testing.T) { t.Parallel() ar := newAnteroomQueue(1) ar.EnqueueZettel(id.Zid(1)) ar.Reset() action, zid, _ := ar.Dequeue() if action != arReload || zid != id.Invalid { t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) } ar.Reload(idset.New(3, 4)) ar.EnqueueZettel(id.Zid(5)) ar.EnqueueZettel(id.Zid(5)) if ar.first == ar.last || ar.first.next != ar.last /*|| ar.first.next.next != ar.last*/ { t.Errorf("Expected 2 rooms") } action, zid1, _ := ar.Dequeue() if action != arZettel { t.Errorf("Expected arZettel, but got %v", action) } action, zid2, _ := ar.Dequeue() if action != arZettel { t.Errorf("Expected arZettel, but got %v", action) } if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) { t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2) } action, zid, _ = ar.Dequeue() if zid != id.Zid(5) || action != arZettel { t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnteroomQueue(1) ar.Reload(idset.New(id.Zid(6))) action, zid, _ = ar.Dequeue() if zid != id.Zid(6) || action != arZettel { t.Errorf("Expected 6/arZettel, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnteroomQueue(1) ar.EnqueueZettel(id.Zid(8)) ar.Reload(nil) action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } } |
Added internal/box/manager/box.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "context" "errors" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) // Conatains all box.Box related functions // Location returns some information where the box is located. func (mgr *Manager) Location() string { if len(mgr.boxes) <= 2 { return "NONE" } var sb strings.Builder for i := range len(mgr.boxes) - 2 { if i > 0 { sb.WriteString(", ") } sb.WriteString(mgr.boxes[i].Location()) } return sb.String() } // CanCreateZettel returns true, if box could possibly create a new zettel. func (mgr *Manager) CanCreateZettel(ctx context.Context) bool { if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { return box.CanCreateZettel(ctx) } return false } // CreateZettel creates a new zettel. func (mgr *Manager) CreateZettel(ctx context.Context, ztl zettel.Zettel) (id.Zid, error) { mgr.mgrLog.Debug().Msg("CreateZettel") if err := mgr.checkContinue(ctx); err != nil { return id.Invalid, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { ztl.Meta = mgr.cleanMetaProperties(ztl.Meta) zid, err := box.CreateZettel(ctx, ztl) if err == nil { mgr.idxUpdateZettel(ctx, ztl) } return zid, err } return id.Invalid, box.ErrReadOnly } // GetZettel retrieves a specific zettel. func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetZettel") if err := mgr.checkContinue(ctx); err != nil { return zettel.Zettel{}, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() return mgr.getZettel(ctx, zid) } func (mgr *Manager) getZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { for i, p := range mgr.boxes { var errZNF box.ErrZettelNotFound if z, err := p.GetZettel(ctx, zid); !errors.As(err, &errZNF) { if err == nil { mgr.Enrich(ctx, z.Meta, i+1) } return z, err } } return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } // GetAllZettel retrieves a specific zettel from all managed boxes. func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetAllZettel") if err := mgr.checkContinue(ctx); err != nil { return nil, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() var result []zettel.Zettel for i, p := range mgr.boxes { if z, err := p.GetZettel(ctx, zid); err == nil { mgr.Enrich(ctx, z.Meta, i+1) result = append(result, z) } } return result, nil } // FetchZids returns the set of all zettel identifer managed by the box. func (mgr *Manager) FetchZids(ctx context.Context) (*idset.Set, error) { mgr.mgrLog.Debug().Msg("FetchZids") if err := mgr.checkContinue(ctx); err != nil { return nil, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() return mgr.fetchZids(ctx) } func (mgr *Manager) fetchZids(ctx context.Context) (*idset.Set, error) { numZettel := 0 for _, p := range mgr.boxes { var mbstats box.ManagedBoxStats p.ReadStats(&mbstats) numZettel += mbstats.Zettel } result := idset.NewCap(numZettel) for _, p := range mgr.boxes { err := p.ApplyZid(ctx, func(zid id.Zid) { result.Add(zid) }, query.AlwaysIncluded) if err != nil { return nil, err } } return result, nil } func (mgr *Manager) hasZettel(ctx context.Context, zid id.Zid) bool { mgr.mgrLog.Debug().Zid(zid).Msg("HasZettel") if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, bx := range mgr.boxes { if bx.HasZettel(ctx, zid) { return true } } return false } // GetMeta returns just the metadata of the zettel with the given identifier. func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta") if err := mgr.checkContinue(ctx); err != nil { return nil, err } m, err := mgr.idxStore.GetMeta(ctx, zid) if err != nil { // TODO: Call GetZettel and return just metadata, in case the index is not complete. return nil, err } mgr.Enrich(ctx, m, 0) return m, nil } // SelectMeta returns all zettel meta data that match the selection // criteria. The result is ordered by descending zettel id. func (mgr *Manager) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) { if msg := mgr.mgrLog.Debug(); msg.Enabled() { msg.Str("query", q.String()).Msg("SelectMeta") } if err := mgr.checkContinue(ctx); err != nil { return nil, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq) if result := compSearch.Result(); result != nil { mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found without ApplyMeta") return result, nil } selected := map[id.Zid]*meta.Meta{} for _, term := range compSearch.Terms { rejected := idset.New() handleMeta := func(m *meta.Meta) { zid := m.Zid if rejected.Contains(zid) { mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected") return } if _, ok := selected[zid]; ok { mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected") return } if compSearch.PreMatch(m) && term.Match(m) { selected[zid] = m mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match") } else { rejected.Add(zid) mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject") } } for _, p := range mgr.boxes { if err2 := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err2 != nil { return nil, err2 } } } result := make([]*meta.Meta, 0, len(selected)) for _, m := range selected { result = append(result, m) } result = compSearch.AfterSearch(result) mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found with ApplyMeta") return result, nil } // CanUpdateZettel returns true, if box could possibly update the given zettel. func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool { if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { return box.CanUpdateZettel(ctx, zettel) } return false } // UpdateZettel updates an existing zettel. func (mgr *Manager) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel") if err := mgr.checkContinue(ctx); err != nil { return err } return mgr.updateZettel(ctx, zettel) } func (mgr *Manager) updateZettel(ctx context.Context, zettel zettel.Zettel) error { if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { zettel.Meta = mgr.cleanMetaProperties(zettel.Meta) if err := box.UpdateZettel(ctx, zettel); err != nil { return err } mgr.idxUpdateZettel(ctx, zettel) return nil } return box.ErrReadOnly } // CanDeleteZettel returns true, if box could possibly delete the given zettel. func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { if p.CanDeleteZettel(ctx, zid) { return true } } return false } // DeleteZettel removes the zettel from the box. func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { mgr.mgrLog.Debug().Zid(zid).Msg("DeleteZettel") if err := mgr.checkContinue(ctx); err != nil { return err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { err := p.DeleteZettel(ctx, zid) if err == nil { mgr.idxDeleteZettel(ctx, zid) return err } var errZNF box.ErrZettelNotFound if !errors.As(err, &errZNF) && !errors.Is(err, box.ErrReadOnly) { return err } } return box.ErrZettelNotFound{Zid: zid} } // Remove all (computed) properties from metadata before storing the zettel. func (mgr *Manager) cleanMetaProperties(m *meta.Meta) *meta.Meta { result := m.Clone() for key := range result.ComputedRest() { if mgr.propertyKeys.Contains(key) { result.Delete(key) } } return result } |
Added internal/box/manager/collect.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box/manager/store" "zettelstore.de/z/strfun" ) type collectData struct { refs *idset.Set words store.WordSet urls store.WordSet } func (data *collectData) initialize() { data.refs = idset.New() data.words = store.NewWordSet() data.urls = store.NewWordSet() } func collectZettelIndexData(zn *ast.ZettelNode, data *collectData) { ast.Walk(data, &zn.BlocksAST) } func (data *collectData) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.VerbatimNode: data.addText(string(n.Content)) case *ast.TranscludeNode: data.addRef(n.Ref) case *ast.TextNode: data.addText(n.Text) case *ast.LinkNode: data.addRef(n.Ref) case *ast.EmbedRefNode: data.addRef(n.Ref) case *ast.CiteNode: data.addText(n.Key) case *ast.LiteralNode: data.addText(string(n.Content)) } return data } func (data *collectData) addText(s string) { for _, word := range strfun.NormalizeWords(s) { data.words.Add(word) } } func (data *collectData) addRef(ref *ast.Reference) { if ref == nil { return } if ref.IsExternal() { data.urls.Add(strings.ToLower(ref.Value)) } if !ref.IsZettel() { return } if zid, err := id.Parse(ref.URL.Path); err == nil { data.refs.Add(zid) } } |
Added internal/box/manager/enrich.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "context" "strconv" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" ) // Enrich computes additional properties and updates the given metadata. func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) { // Calculate computed, but stored values. if _, hasCreated := m.Get(meta.KeyCreated); !hasCreated { m.Set(meta.KeyCreated, computeCreated(m.Zid)) } if box.DoEnrich(ctx) { computePublished(m) if boxNumber > 0 { m.Set(meta.KeyBoxNumber, meta.Value(strconv.Itoa(boxNumber))) } mgr.idxStore.Enrich(ctx, m) } } func computeCreated(zid id.Zid) meta.Value { if zid <= 10101000000 { // A year 0000 is not allowed and therefore an artificial Zid. // In the year 0001, the month must be > 0. // In the month 000101, the day must be > 0. return "00010101000000" } seconds := min(zid%100, 59) zid /= 100 minutes := min(zid%100, 59) zid /= 100 hours := min(zid%100, 23) zid /= 100 day := zid % 100 zid /= 100 month := zid % 100 year := zid / 100 month, day = sanitizeMonthDay(year, month, day) created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds return meta.Value(created.String()) } func sanitizeMonthDay(year, month, day id.Zid) (id.Zid, id.Zid) { if day < 1 { day = 1 } if month < 1 { month = 1 } if month > 12 { month = 12 } switch month { case 1, 3, 5, 7, 8, 10, 12: if day > 31 { day = 31 } case 4, 6, 9, 11: if day > 30 { day = 30 } case 2: if year%4 != 0 || (year%100 == 0 && year%400 != 0) { if day > 28 { day = 28 } } else { if day > 29 { day = 29 } } } return month, day } func computePublished(m *meta.Meta) { if _, ok := m.Get(meta.KeyPublished); ok { return } if modified, ok := m.Get(meta.KeyModified); ok { if _, ok = modified.AsTime(); ok { m.Set(meta.KeyPublished, modified) return } } if created, ok := m.Get(meta.KeyCreated); ok { if _, ok = created.AsTime(); ok { m.Set(meta.KeyPublished, created) return } } zidValue := meta.Value(m.Zid.String()) if _, ok := zidValue.AsTime(); ok { m.Set(meta.KeyPublished, zidValue) return } // Neither the zettel was modified nor the zettel identifer contains a valid // timestamp. In this case do not set the "published" property. } |
Added internal/box/manager/indexer.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "context" "fmt" "net/url" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager/store" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/zettel" "zettelstore.de/z/strfun" ) // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchEqual(word string) *idset.Set { found := mgr.idxStore.SearchEqual(word) mgr.idxLog.Debug().Str("word", word).Int("found", int64(found.Length())).Msg("SearchEqual") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // SearchPrefix returns all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchPrefix(prefix string) *idset.Set { found := mgr.idxStore.SearchPrefix(prefix) mgr.idxLog.Debug().Str("prefix", prefix).Int("found", int64(found.Length())).Msg("SearchPrefix") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // SearchSuffix returns all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchSuffix(suffix string) *idset.Set { found := mgr.idxStore.SearchSuffix(suffix) mgr.idxLog.Debug().Str("suffix", suffix).Int("found", int64(found.Length())).Msg("SearchSuffix") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchContains(s string) *idset.Set { found := mgr.idxStore.SearchContains(s) mgr.idxLog.Debug().Str("s", s).Int("found", int64(found.Length())).Msg("SearchContains") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // idxIndexer runs in the background and updates the index data structures. // This is the main service of the idxIndexer. func (mgr *Manager) idxIndexer() { // Something may panic. Ensure a running indexer. defer func() { if ri := recover(); ri != nil { kernel.Main.LogRecover("Indexer", ri) go mgr.idxIndexer() } }() timerDuration := 15 * time.Second timer := time.NewTimer(timerDuration) ctx := box.NoEnrichContext(context.Background()) for { mgr.idxWorkService(ctx) if !mgr.idxSleepService(timer, timerDuration) { return } } } func (mgr *Manager) idxWorkService(ctx context.Context) { var start time.Time for { switch action, zid, lastReload := mgr.idxAr.Dequeue(); action { case arNothing: return case arReload: mgr.idxLog.Debug().Msg("reload") zids, err := mgr.FetchZids(ctx) if err == nil { start = time.Now() mgr.idxAr.Reload(zids) mgr.idxMx.Lock() mgr.idxLastReload = time.Now().Local() mgr.idxSinceReload = 0 mgr.idxMx.Unlock() } case arZettel: mgr.idxLog.Debug().Zid(zid).Msg("zettel") zettel, err := mgr.GetZettel(ctx, zid) if err != nil { // Zettel was deleted or is not accessible b/c of other reasons mgr.idxLog.Trace().Zid(zid).Msg("delete") mgr.idxDeleteZettel(ctx, zid) continue } mgr.idxLog.Trace().Zid(zid).Msg("update") mgr.idxUpdateZettel(ctx, zettel) mgr.idxMx.Lock() if lastReload { mgr.idxDurReload = time.Since(start) } mgr.idxSinceReload++ mgr.idxMx.Unlock() } } } func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool { select { case _, ok := <-mgr.idxReady: if !ok { return false } case _, ok := <-timer.C: if !ok { return false } // mgr.idxStore.Optimize() // TODO: make it less often, for example once per 10 minutes timer.Reset(timerDuration) case <-mgr.done: if !timer.Stop() { <-timer.C } return false } return true } func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) { var cData collectData cData.initialize() if mustIndexZettel(zettel.Meta) { collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) } m := zettel.Meta zi := store.NewZettelIndex(m) mgr.idxCollectFromMeta(ctx, m, zi, &cData) mgr.idxProcessData(ctx, zi, &cData) toCheck := mgr.idxStore.UpdateReferences(ctx, zi) mgr.idxCheckZettel(toCheck) } func mustIndexZettel(m *meta.Meta) bool { return m.Zid >= id.Zid(999999900) } func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { for key, val := range m.Computed() { descr := meta.GetDescription(key) if descr.IsProperty() { continue } switch descr.Type { case meta.TypeID: mgr.idxUpdateValue(ctx, descr.Inverse, string(val), zi) case meta.TypeIDSet: for val := range val.Fields() { mgr.idxUpdateValue(ctx, descr.Inverse, val, zi) } case meta.TypeURL: if _, err := url.Parse(string(val)); err == nil { cData.urls.Add(string(val)) } default: if descr.Type.IsSet { for val := range val.Fields() { idxCollectMetaValue(cData.words, val) } } else { idxCollectMetaValue(cData.words, string(val)) } } } } func idxCollectMetaValue(stWords store.WordSet, value string) { if words := strfun.NormalizeWords(value); len(words) > 0 { for _, word := range words { stWords.Add(word) } } else { stWords.Add(value) } } func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) { cData.refs.ForEach(func(ref id.Zid) { if mgr.hasZettel(ctx, ref) { zi.AddBackRef(ref) } else { zi.AddDeadRef(ref) } }) zi.SetWords(cData.words) zi.SetUrls(cData.urls) } func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } if !mgr.hasZettel(ctx, zid) { zi.AddDeadRef(zid) return } if inverseKey == "" { zi.AddBackRef(zid) return } zi.AddInverseRef(inverseKey, zid) } func (mgr *Manager) idxDeleteZettel(ctx context.Context, zid id.Zid) { toCheck := mgr.idxStore.DeleteZettel(ctx, zid) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCheckZettel(s *idset.Set) { s.ForEach(func(zid id.Zid) { mgr.idxAr.EnqueueZettel(zid) }) } |
Added internal/box/manager/manager.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "io" "net/url" "sync" "time" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager/mapstore" "zettelstore.de/z/internal/box/manager/store" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logger" ) // ConnectData contains all administration related values. type ConnectData struct { Number int // number of the box, starting with 1. Config config.Config Enricher box.Enricher Notify box.UpdateNotifier } // Connect returns a handle to the specified box. func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (box.ManagedBox, error) { if authManager.IsReadonly() { rawURL := u.String() // TODO: the following is wrong under some circumstances: // 1. fragment is set if q := u.Query(); len(q) == 0 { rawURL += "?readonly" } else if _, ok := q["readonly"]; !ok { rawURL += "&readonly" } var err error if u, err = url.Parse(rawURL); err != nil { return nil, err } } if create, ok := registry[u.Scheme]; ok { return create(u, cdata) } return nil, &ErrInvalidScheme{u.Scheme} } // ErrInvalidScheme is returned if there is no box with the given scheme. type ErrInvalidScheme struct{ Scheme string } func (err *ErrInvalidScheme) Error() string { return "Invalid scheme: " + err.Scheme } type createFunc func(*url.URL, *ConnectData) (box.ManagedBox, error) var registry = map[string]createFunc{} // Register the encoder for later retrieval. func Register(scheme string, create createFunc) { if _, ok := registry[scheme]; ok { panic(scheme) } registry[scheme] = create } // Manager is a coordinating box. type Manager struct { mgrLog *logger.Logger stateMx sync.RWMutex state box.StartState mgrMx sync.RWMutex rtConfig config.Config boxes []box.ManagedBox observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} infos chan box.UpdateInfo propertyKeys *set.Set[string] // Set of property key names // Indexer data idxLog *logger.Logger idxStore store.Store idxAr *anteroomQueue idxReady chan struct{} // Signal a non-empty anteroom to background task // Indexer stats data idxMx sync.RWMutex idxLastReload time.Time idxDurReload time.Duration idxSinceReload uint64 } func (mgr *Manager) setState(newState box.StartState) { mgr.stateMx.Lock() mgr.state = newState mgr.stateMx.Unlock() } // State returns the box.StartState of the manager. func (mgr *Manager) State() box.StartState { mgr.stateMx.RLock() state := mgr.state mgr.stateMx.RUnlock() return state } // New creates a new managing box. func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) { descrs := meta.GetSortedKeyDescriptions() propertyKeys := set.New[string]() for _, kd := range descrs { if kd.IsProperty() { propertyKeys.Add(kd.Name) } } boxLog := kernel.Main.GetLogger(kernel.BoxService) mgr := &Manager{ mgrLog: boxLog.Clone().Str("box", "manager").Child(), rtConfig: rtConfig, infos: make(chan box.UpdateInfo, len(boxURIs)*10), propertyKeys: propertyKeys, idxLog: boxLog.Clone().Str("box", "index").Child(), idxStore: createIdxStore(rtConfig), idxAr: newAnteroomQueue(1000), idxReady: make(chan struct{}, 1), } cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.notifyChanged} boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) for _, uri := range boxURIs { p, err := Connect(uri, authManager, &cdata) if err != nil { return nil, err } if p != nil { boxes = append(boxes, p) cdata.Number++ } } constbox, err := registry[" const"](nil, &cdata) if err != nil { return nil, err } cdata.Number++ compbox, err := registry[" comp"](nil, &cdata) if err != nil { return nil, err } cdata.Number++ boxes = append(boxes, constbox, compbox) mgr.boxes = boxes return mgr, nil } func createIdxStore(_ config.Config) store.Store { return mapstore.New() } // RegisterObserver registers an observer that will be notified // if a zettel was found to be changed. func (mgr *Manager) RegisterObserver(f box.UpdateFunc) { if f != nil { mgr.mxObserver.Lock() mgr.observers = append(mgr.observers, f) mgr.mxObserver.Unlock() } } func (mgr *Manager) notifier() { // The call to notify may panic. Ensure a running notifier. defer func() { if ri := recover(); ri != nil { kernel.Main.LogRecover("Notifier", ri) go mgr.notifier() } }() tsLastEvent := time.Now() cache := destutterCache{} for { select { case ci, ok := <-mgr.infos: if ok { now := time.Now() if len(cache) > 1 && tsLastEvent.Add(10*time.Second).Before(now) { // Cache contains entries and is definitely outdated mgr.mgrLog.Trace().Msg("clean destutter cache") cache = destutterCache{} } tsLastEvent = now reason, zid := ci.Reason, ci.Zid mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier") if ignoreUpdate(cache, now, reason, zid) { mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored") continue } isStarted := mgr.State() == box.StartStateStarted mgr.idxEnqueue(reason, zid) if ci.Box == nil { ci.Box = mgr } if isStarted { mgr.notifyObserver(&ci) } } case <-mgr.done: return } } } type destutterData struct { deadAt time.Time reason box.UpdateReason } type destutterCache = map[id.Zid]destutterData func ignoreUpdate(cache destutterCache, now time.Time, reason box.UpdateReason, zid id.Zid) bool { if dsd, found := cache[zid]; found { if dsd.reason == reason && dsd.deadAt.After(now) { return true } } cache[zid] = destutterData{ deadAt: now.Add(500 * time.Millisecond), reason: reason, } return false } func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { switch reason { case box.OnReady: return case box.OnReload: mgr.idxAr.Reset() case box.OnZettel: mgr.idxAr.EnqueueZettel(zid) case box.OnDelete: mgr.idxAr.EnqueueZettel(zid) default: mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") return } select { case mgr.idxReady <- struct{}{}: default: } } func (mgr *Manager) notifyObserver(ci *box.UpdateInfo) { mgr.mxObserver.RLock() observers := mgr.observers mgr.mxObserver.RUnlock() for _, ob := range observers { ob(*ci) } } // Start the box. Now all other functions of the box are allowed. // Starting an already started box is not allowed. func (mgr *Manager) Start(ctx context.Context) error { mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() if mgr.State() != box.StartStateStopped { return box.ErrStarted } mgr.setState(box.StartStateStarting) for i := len(mgr.boxes) - 1; i >= 0; i-- { ssi, ok := mgr.boxes[i].(box.StartStopper) if !ok { continue } err := ssi.Start(ctx) if err == nil { continue } mgr.setState(box.StartStateStopping) for j := i + 1; j < len(mgr.boxes); j++ { if ssj, ok2 := mgr.boxes[j].(box.StartStopper); ok2 { ssj.Stop(ctx) } } mgr.setState(box.StartStateStopped) return err } mgr.idxAr.Reset() // Ensure an initial index run mgr.done = make(chan struct{}) go mgr.notifier() mgr.waitBoxesAreStarted() mgr.setState(box.StartStateStarted) mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady}) go mgr.idxIndexer() return nil } func (mgr *Manager) waitBoxesAreStarted() { const waitTime = 10 * time.Millisecond const waitLoop = int(1 * time.Second / waitTime) for i := 1; !mgr.allBoxesStarted(); i++ { if i%waitLoop == 0 { if time.Duration(i)*waitTime > time.Minute { mgr.mgrLog.Info().Msg("Waiting for more than one minute to start") } else { mgr.mgrLog.Trace().Msg("Wait for boxes to start") } } time.Sleep(waitTime) } } func (mgr *Manager) allBoxesStarted() bool { for _, bx := range mgr.boxes { if b, ok := bx.(box.StartStopper); ok && b.State() != box.StartStateStarted { return false } } return true } // Stop the started box. Now only the Start() function is allowed. func (mgr *Manager) Stop(ctx context.Context) { mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() if err := mgr.checkContinue(ctx); err != nil { return } mgr.setState(box.StartStateStopping) close(mgr.done) for _, p := range mgr.boxes { if ss, ok := p.(box.StartStopper); ok { ss.Stop(ctx) } } mgr.setState(box.StartStateStopped) } // Refresh internal box data. func (mgr *Manager) Refresh(ctx context.Context) error { mgr.mgrLog.Debug().Msg("Refresh") if err := mgr.checkContinue(ctx); err != nil { return err } mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() for _, bx := range mgr.boxes { if rb, ok := bx.(box.Refresher); ok { rb.Refresh(ctx) } } return nil } // ReIndex data of the given zettel. func (mgr *Manager) ReIndex(ctx context.Context, zid id.Zid) error { mgr.mgrLog.Debug().Msg("ReIndex") if err := mgr.checkContinue(ctx); err != nil { return err } mgr.infos <- box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: zid} return nil } // ReadStats populates st with box statistics. func (mgr *Manager) ReadStats(st *box.Stats) { mgr.mgrLog.Debug().Msg("ReadStats") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() subStats := make([]box.ManagedBoxStats, len(mgr.boxes)) for i, p := range mgr.boxes { p.ReadStats(&subStats[i]) } st.ReadOnly = true sumZettel := 0 for _, sst := range subStats { if !sst.ReadOnly { st.ReadOnly = false } sumZettel += sst.Zettel } st.NumManagedBoxes = len(mgr.boxes) st.ZettelTotal = sumZettel var storeSt store.Stats mgr.idxMx.RLock() defer mgr.idxMx.RUnlock() mgr.idxStore.ReadStats(&storeSt) st.LastReload = mgr.idxLastReload st.IndexesSinceReload = mgr.idxSinceReload st.DurLastReload = mgr.idxDurReload st.ZettelIndexed = storeSt.Zettel st.IndexUpdates = storeSt.Updates st.IndexedWords = storeSt.Words st.IndexedUrls = storeSt.Urls } // Dump internal data structures to a Writer. func (mgr *Manager) Dump(w io.Writer) { mgr.idxStore.Dump(w) } func (mgr *Manager) checkContinue(ctx context.Context) error { if mgr.State() != box.StartStateStarted { return box.ErrStopped } return ctx.Err() } func (mgr *Manager) notifyChanged(bbox box.BaseBox, zid id.Zid, reason box.UpdateReason) { if infos := mgr.infos; infos != nil { mgr.infos <- box.UpdateInfo{Box: bbox, Reason: reason, Zid: zid} } } |
Added internal/box/manager/mapstore/mapstore.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package mapstore stored the index in main memory via a Go map. package mapstore import ( "context" "fmt" "io" "maps" "slices" "strings" "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager/store" ) type zettelData struct { meta *meta.Meta // a local copy of the metadata, without computed keys dead *idset.Set // set of dead references in this zettel forward *idset.Set // set of forward references in this zettel backward *idset.Set // set of zettel that reference with zettel otherRefs map[string]bidiRefs words []string // list of words of this zettel urls []string // list of urls of this zettel } type bidiRefs struct { forward *idset.Set backward *idset.Set } func (zd *zettelData) optimize() { zd.dead.Optimize() zd.forward.Optimize() zd.backward.Optimize() for _, bidi := range zd.otherRefs { bidi.forward.Optimize() bidi.backward.Optimize() } } type mapStore struct { mx sync.RWMutex intern map[string]string // map to intern strings idx map[id.Zid]*zettelData dead map[id.Zid]*idset.Set // map dead refs where they occur words stringRefs urls stringRefs // Stats mxStats sync.Mutex updates uint64 } type stringRefs map[string]*idset.Set // New returns a new memory-based index store. func New() store.Store { return &mapStore{ intern: make(map[string]string, 1024), idx: make(map[id.Zid]*zettelData), dead: make(map[id.Zid]*idset.Set), words: make(stringRefs), urls: make(stringRefs), } } func (ms *mapStore) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { ms.mx.RLock() defer ms.mx.RUnlock() if zi, found := ms.idx[zid]; found && zi.meta != nil { // zi.meta is nil, if zettel was referenced, but is not indexed yet. return zi.meta.Clone(), nil } return nil, box.ErrZettelNotFound{Zid: zid} } func (ms *mapStore) Enrich(_ context.Context, m *meta.Meta) { if ms.doEnrich(m) { ms.mxStats.Lock() ms.updates++ ms.mxStats.Unlock() } } func (ms *mapStore) doEnrich(m *meta.Meta) bool { ms.mx.RLock() defer ms.mx.RUnlock() zi, ok := ms.idx[m.Zid] if !ok { return false } var updated bool if !zi.dead.IsEmpty() { m.Set(meta.KeyDead, zi.dead.MetaValue()) updated = true } back := removeOtherMetaRefs(m, zi.backward.Clone()) if !zi.backward.IsEmpty() { m.Set(meta.KeyBackward, zi.backward.MetaValue()) updated = true } if !zi.forward.IsEmpty() { m.Set(meta.KeyForward, zi.forward.MetaValue()) back.ISubstract(zi.forward) updated = true } for k, refs := range zi.otherRefs { if !refs.backward.IsEmpty() { m.Set(k, refs.backward.MetaValue()) back.ISubstract(refs.backward) updated = true } } if !back.IsEmpty() { m.Set(meta.KeyBack, back.MetaValue()) updated = true } return updated } // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (ms *mapStore) SearchEqual(word string) *idset.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := idset.New() if refs, ok := ms.words[word]; ok { result = result.IUnion(refs) } if refs, ok := ms.urls[word]; ok { result = result.IUnion(refs) } zid, err := id.Parse(word) if err != nil { return result } zi, ok := ms.idx[zid] if !ok { return result } return addBackwardZids(result, zid, zi) } // SearchPrefix returns all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. func (ms *mapStore) SearchPrefix(prefix string) *idset.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(prefix, strings.HasPrefix) l := len(prefix) if l > 14 { return result } maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) if err != nil { return result } var minZid id.Zid if l < 14 && prefix == "0000000000000"[:l] { minZid = id.Zid(1) } else { minZid, err = id.Parse(prefix + "00000000000000"[:14-l]) if err != nil { return result } } for zid, zi := range ms.idx { if minZid <= zid && zid <= maxZid { result = addBackwardZids(result, zid, zi) } } return result } // SearchSuffix returns all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. func (ms *mapStore) SearchSuffix(suffix string) *idset.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(suffix, strings.HasSuffix) l := len(suffix) if l > 14 { return result } val, err := id.ParseUint(suffix) if err != nil { return result } modulo := uint64(1) for range l { modulo *= 10 } for zid, zi := range ms.idx { if uint64(zid)%modulo == val { result = addBackwardZids(result, zid, zi) } } return result } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (ms *mapStore) SearchContains(s string) *idset.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(s, strings.Contains) if len(s) > 14 { return result } if _, err := id.ParseUint(s); err != nil { return result } for zid, zi := range ms.idx { if strings.Contains(zid.String(), s) { result = addBackwardZids(result, zid, zi) } } return result } func (ms *mapStore) selectWithPred(s string, pred func(string, string) bool) *idset.Set { // Must only be called if ms.mx is read-locked! result := idset.New() for word, refs := range ms.words { if !pred(word, s) { continue } result.IUnion(refs) } for u, refs := range ms.urls { if !pred(u, s) { continue } result.IUnion(refs) } return result } func addBackwardZids(result *idset.Set, zid id.Zid, zi *zettelData) *idset.Set { // Must only be called if ms.mx is read-locked! result = result.Add(zid) result = result.IUnion(zi.backward) for _, mref := range zi.otherRefs { result = result.IUnion(mref.backward) } return result } func removeOtherMetaRefs(m *meta.Meta, back *idset.Set) *idset.Set { for key, val := range m.Rest() { switch meta.Type(key) { case meta.TypeID: if zid, err := id.Parse(string(val)); err == nil { back = back.Remove(zid) } case meta.TypeIDSet: for val := range val.Fields() { if zid, err := id.Parse(val); err == nil { back = back.Remove(zid) } } } } return back } func (ms *mapStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) *idset.Set { ms.mx.Lock() defer ms.mx.Unlock() m := ms.makeMeta(zidx) zi, ziExist := ms.idx[zidx.Zid] if !ziExist || zi == nil { zi = &zettelData{} ziExist = false } // Is this zettel an old dead reference mentioned in other zettel? var toCheck *idset.Set if refs, ok := ms.dead[zidx.Zid]; ok { // These must be checked later again toCheck = refs delete(ms.dead, zidx.Zid) } zi.meta = m ms.updateDeadReferences(zidx, zi) ids := ms.updateForwardBackwardReferences(zidx, zi) toCheck = toCheck.IUnion(ids) ids = ms.updateMetadataReferences(zidx, zi) toCheck = toCheck.IUnion(ids) zi.words = updateStrings(zidx.Zid, ms.words, zi.words, zidx.GetWords()) zi.urls = updateStrings(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) // Check if zi must be inserted into ms.idx if !ziExist { ms.idx[zidx.Zid] = zi } zi.optimize() return toCheck } var internableKeys = map[string]bool{ meta.KeyRole: true, meta.KeySyntax: true, meta.KeyFolgeRole: true, meta.KeyLang: true, meta.KeyReadOnly: true, } func isInternableValue(key string) bool { if internableKeys[key] { return true } return strings.HasSuffix(key, meta.SuffixKeyRole) } func (ms *mapStore) internString(s string) string { if is, found := ms.intern[s]; found { return is } ms.intern[s] = s return s } func (ms *mapStore) makeMeta(zidx *store.ZettelIndex) *meta.Meta { origM := zidx.GetMeta() copyM := meta.New(origM.Zid) for key, val := range origM.All() { key = ms.internString(key) if isInternableValue(key) { copyM.Set(key, meta.Value(ms.internString(string(val)))) } else if key == meta.KeyBoxNumber || !meta.IsComputed(key) { copyM.Set(key, val) } } return copyM } func (ms *mapStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelData) { // Must only be called if ms.mx is write-locked! drefs := zidx.GetDeadRefs() newRefs, remRefs := zi.dead.Diff(drefs) zi.dead = drefs remRefs.ForEach(func(ref id.Zid) { ms.dead[ref] = ms.dead[ref].Remove(zidx.Zid) }) newRefs.ForEach(func(ref id.Zid) { ms.dead[ref] = ms.dead[ref].Add(zidx.Zid) }) } func (ms *mapStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelData) *idset.Set { // Must only be called if ms.mx is write-locked! brefs := zidx.GetBackRefs() newRefs, remRefs := zi.forward.Diff(brefs) zi.forward = brefs var toCheck *idset.Set remRefs.ForEach(func(ref id.Zid) { bzi := ms.getOrCreateEntry(ref) bzi.backward = bzi.backward.Remove(zidx.Zid) if bzi.meta == nil { toCheck = toCheck.Add(ref) } }) newRefs.ForEach(func(ref id.Zid) { bzi := ms.getOrCreateEntry(ref) bzi.backward = bzi.backward.Add(zidx.Zid) if bzi.meta == nil { toCheck = toCheck.Add(ref) } }) return toCheck } func (ms *mapStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelData) *idset.Set { // Must only be called if ms.mx is write-locked! inverseRefs := zidx.GetInverseRefs() for key, mr := range zi.otherRefs { if _, ok := inverseRefs[key]; ok { continue } ms.removeInverseMeta(zidx.Zid, key, mr.forward) } if zi.otherRefs == nil { zi.otherRefs = make(map[string]bidiRefs) } var toCheck *idset.Set for key, mrefs := range inverseRefs { mr := zi.otherRefs[key] newRefs, remRefs := mr.forward.Diff(mrefs) mr.forward = mrefs zi.otherRefs[key] = mr newRefs.ForEach(func(ref id.Zid) { bzi := ms.getOrCreateEntry(ref) if bzi.otherRefs == nil { bzi.otherRefs = make(map[string]bidiRefs) } bmr := bzi.otherRefs[key] bmr.backward = bmr.backward.Add(zidx.Zid) bzi.otherRefs[key] = bmr if bzi.meta == nil { toCheck = toCheck.Add(ref) } }) ms.removeInverseMeta(zidx.Zid, key, remRefs) } return toCheck } func updateStrings(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { newWords, removeWords := next.Diff(prev) for _, word := range newWords { srefs[word] = srefs[word].Add(zid) } for _, word := range removeWords { refs, ok := srefs[word] if !ok { continue } refs = refs.Remove(zid) if refs.IsEmpty() { delete(srefs, word) continue } srefs[word] = refs } return next.Words() } func (ms *mapStore) getOrCreateEntry(zid id.Zid) *zettelData { // Must only be called if ms.mx is write-locked! if zi, ok := ms.idx[zid]; ok { return zi } zi := &zettelData{} ms.idx[zid] = zi return zi } func (ms *mapStore) DeleteZettel(_ context.Context, zid id.Zid) *idset.Set { ms.mx.Lock() defer ms.mx.Unlock() return ms.doDeleteZettel(zid) } func (ms *mapStore) doDeleteZettel(zid id.Zid) *idset.Set { // Must only be called if ms.mx is write-locked! zi, ok := ms.idx[zid] if !ok { return nil } ms.deleteDeadSources(zid, zi) toCheck := ms.deleteForwardBackward(zid, zi) for key, mrefs := range zi.otherRefs { ms.removeInverseMeta(zid, key, mrefs.forward) } deleteStrings(ms.words, zi.words, zid) deleteStrings(ms.urls, zi.urls, zid) delete(ms.idx, zid) return toCheck } func (ms *mapStore) deleteDeadSources(zid id.Zid, zi *zettelData) { // Must only be called if ms.mx is write-locked! zi.dead.ForEach(func(ref id.Zid) { if drefs, ok := ms.dead[ref]; ok { if drefs = drefs.Remove(zid); drefs.IsEmpty() { delete(ms.dead, ref) } else { ms.dead[ref] = drefs } } }) } func (ms *mapStore) deleteForwardBackward(zid id.Zid, zi *zettelData) *idset.Set { // Must only be called if ms.mx is write-locked! zi.forward.ForEach(func(ref id.Zid) { if fzi, ok := ms.idx[ref]; ok { fzi.backward = fzi.backward.Remove(zid) } }) var toCheck *idset.Set zi.backward.ForEach(func(ref id.Zid) { if bzi, ok := ms.idx[ref]; ok { bzi.forward = bzi.forward.Remove(zid) toCheck = toCheck.Add(ref) } }) return toCheck } func (ms *mapStore) removeInverseMeta(zid id.Zid, key string, forward *idset.Set) { // Must only be called if ms.mx is write-locked! forward.ForEach(func(ref id.Zid) { bzi, ok := ms.idx[ref] if !ok || bzi.otherRefs == nil { return } bmr, ok := bzi.otherRefs[key] if !ok { return } bmr.backward = bmr.backward.Remove(zid) if !bmr.backward.IsEmpty() || !bmr.forward.IsEmpty() { bzi.otherRefs[key] = bmr } else { delete(bzi.otherRefs, key) if len(bzi.otherRefs) == 0 { bzi.otherRefs = nil } } }) } func deleteStrings(msStringMap stringRefs, curStrings []string, zid id.Zid) { // Must only be called if ms.mx is write-locked! for _, word := range curStrings { refs, ok := msStringMap[word] if !ok { continue } refs = refs.Remove(zid) if refs.IsEmpty() { delete(msStringMap, word) continue } msStringMap[word] = refs } } func (ms *mapStore) Optimize() { ms.mx.Lock() defer ms.mx.Unlock() // No need to optimize ms.idx: is already done via ms.UpdateReferences for _, dead := range ms.dead { dead.Optimize() } for _, s := range ms.words { s.Optimize() } for _, s := range ms.urls { s.Optimize() } } func (ms *mapStore) ReadStats(st *store.Stats) { ms.mx.RLock() st.Zettel = len(ms.idx) st.Words = uint64(len(ms.words)) st.Urls = uint64(len(ms.urls)) ms.mx.RUnlock() ms.mxStats.Lock() st.Updates = ms.updates ms.mxStats.Unlock() } func (ms *mapStore) Dump(w io.Writer) { ms.mx.RLock() defer ms.mx.RUnlock() io.WriteString(w, "=== Dump\n") ms.dumpIndex(w) ms.dumpDead(w) dumpStringRefs(w, "Words", "", "", ms.words) dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) } func (ms *mapStore) dumpIndex(w io.Writer) { if len(ms.idx) == 0 { return } io.WriteString(w, "==== Zettel Index\n") zids := make([]id.Zid, 0, len(ms.idx)) for id := range ms.idx { zids = append(zids, id) } slices.Sort(zids) for _, id := range zids { fmt.Fprintln(w, "=====", id) zi := ms.idx[id] if !zi.dead.IsEmpty() { fmt.Fprintln(w, "* Dead:", zi.dead) } dumpSet(w, "* Forward:", zi.forward) dumpSet(w, "* Backward:", zi.backward) otherRefs := make([]string, 0, len(zi.otherRefs)) for k := range zi.otherRefs { otherRefs = append(otherRefs, k) } slices.Sort(otherRefs) for _, k := range otherRefs { fmt.Fprintln(w, "* Meta", k) dumpSet(w, "** Forward:", zi.otherRefs[k].forward) dumpSet(w, "** Backward:", zi.otherRefs[k].backward) } dumpStrings(w, "* Words", "", "", zi.words) dumpStrings(w, "* URLs", "[[", "]]", zi.urls) } } func (ms *mapStore) dumpDead(w io.Writer) { if len(ms.dead) == 0 { return } fmt.Fprintf(w, "==== Dead References\n") zids := make([]id.Zid, 0, len(ms.dead)) for id := range ms.dead { zids = append(zids, id) } slices.Sort(zids) for _, id := range zids { fmt.Fprintln(w, ";", id) fmt.Fprintln(w, ":", ms.dead[id]) } } func dumpSet(w io.Writer, prefix string, s *idset.Set) { if !s.IsEmpty() { io.WriteString(w, prefix) s.ForEach(func(zid id.Zid) { io.WriteString(w, " ") w.Write(zid.Bytes()) }) fmt.Fprintln(w) } } func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { if len(slice) > 0 { sl := make([]string, len(slice)) copy(sl, slice) slices.Sort(sl) fmt.Fprintln(w, title) for _, s := range sl { fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) } } } func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { if len(srefs) == 0 { return } fmt.Fprintln(w, "====", title) for _, s := range slices.Sorted(maps.Keys(srefs)) { fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) fmt.Fprintln(w, ":", srefs[s]) } } |
Added internal/box/manager/store/store.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package store contains general index data for storing a zettel index. package store import ( "context" "io" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/query" ) // Stats records statistics about the store. type Stats struct { // Zettel is the number of zettel managed by the indexer. Zettel int // Updates count the number of metadata updates. Updates uint64 // Words count the different words stored in the store. Words uint64 // Urls count the different URLs stored in the store. Urls uint64 } // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { query.Searcher // GetMeta returns the metadata of the zettel with the given identifier. GetMeta(context.Context, id.Zid) (*meta.Meta, error) // Entrich metadata with data from store. Enrich(ctx context.Context, m *meta.Meta) // UpdateReferences for a specific zettel. // Returns set of zettel identifier that must also be checked for changes. UpdateReferences(context.Context, *ZettelIndex) *idset.Set // DeleteZettel removes index data for given zettel. // Returns set of zettel identifier that must also be checked for changes. DeleteZettel(context.Context, id.Zid) *idset.Set // Optimize removes unneeded space. Optimize() // ReadStats populates st with store statistics. ReadStats(st *Stats) // Dump the content to a Writer. Dump(io.Writer) } |
Added internal/box/manager/store/wordset.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store // WordSet contains the set of all words, with the count of their occurrences. type WordSet map[string]int // NewWordSet returns a new WordSet. func NewWordSet() WordSet { return make(WordSet) } // Add one word to the set func (ws WordSet) Add(s string) { ws[s] = ws[s] + 1 } // Words gives the slice of all words in the set. func (ws WordSet) Words() []string { if len(ws) == 0 { return nil } words := make([]string, 0, len(ws)) for w := range ws { words = append(words, w) } return words } // Diff calculates the word slice to be added and to be removed from oldWords // to get the given word set. func (ws WordSet) Diff(oldWords []string) (newWords, removeWords []string) { if len(ws) == 0 { return nil, oldWords } if len(oldWords) == 0 { return ws.Words(), nil } oldSet := make(WordSet, len(oldWords)) for _, ow := range oldWords { if _, ok := ws[ow]; ok { oldSet[ow] = 1 continue } removeWords = append(removeWords, ow) } for w := range ws { if _, ok := oldSet[w]; ok { continue } newWords = append(newWords, w) } return newWords, removeWords } |
Added internal/box/manager/store/wordset_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store_test import ( "slices" "testing" "zettelstore.de/z/internal/box/manager/store" ) func equalWordList(exp, got []string) bool { if len(exp) != len(got) { return false } if len(got) == 0 { return len(exp) == 0 } slices.Sort(got) for i, w := range exp { if w != got[i] { return false } } return true } func TestWordsWords(t *testing.T) { t.Parallel() testcases := []struct { words store.WordSet exp []string }{ {nil, nil}, {store.WordSet{}, nil}, {store.WordSet{"a": 1, "b": 2}, []string{"a", "b"}}, } for i, tc := range testcases { got := tc.words.Words() if !equalWordList(tc.exp, got) { t.Errorf("%d: %v.Words() == %v, but got %v", i, tc.words, tc.exp, got) } } } func TestWordsDiff(t *testing.T) { t.Parallel() testcases := []struct { cur store.WordSet old []string expN, expR []string }{ {nil, nil, nil, nil}, {store.WordSet{}, []string{}, nil, nil}, {store.WordSet{"a": 1}, []string{}, []string{"a"}, nil}, {store.WordSet{"a": 1}, []string{"b"}, []string{"a"}, []string{"b"}}, {store.WordSet{}, []string{"b"}, nil, []string{"b"}}, {store.WordSet{"a": 1}, []string{"a"}, nil, nil}, } for i, tc := range testcases { gotN, gotR := tc.cur.Diff(tc.old) if !equalWordList(tc.expN, gotN) { t.Errorf("%d: %v.Diff(%v)->new %v, but got %v", i, tc.cur, tc.old, tc.expN, gotN) } if !equalWordList(tc.expR, gotR) { t.Errorf("%d: %v.Diff(%v)->rem %v, but got %v", i, tc.cur, tc.old, tc.expR, gotR) } } } |
Added internal/box/manager/store/zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store import ( "maps" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" ) // ZettelIndex contains all index data of a zettel. type ZettelIndex struct { Zid id.Zid // zid of the indexed zettel meta *meta.Meta // full metadata backrefs *idset.Set // set of back references inverseRefs map[string]*idset.Set // references of inverse keys deadrefs *idset.Set // set of dead references words WordSet urls WordSet } // NewZettelIndex creates a new zettel index. func NewZettelIndex(m *meta.Meta) *ZettelIndex { return &ZettelIndex{ Zid: m.Zid, meta: m, backrefs: idset.New(), inverseRefs: make(map[string]*idset.Set), deadrefs: idset.New(), } } // AddBackRef adds a reference to a zettel where the current zettel links to // without any more information. func (zi *ZettelIndex) AddBackRef(zid id.Zid) { zi.backrefs.Add(zid) } // AddInverseRef adds a named reference to a zettel. On that zettel, the given // metadata key should point back to the current zettel. func (zi *ZettelIndex) AddInverseRef(key string, zid id.Zid) { if zids, ok := zi.inverseRefs[key]; ok { zids.Add(zid) return } zi.inverseRefs[key] = idset.New(zid) } // AddDeadRef adds a dead reference to a zettel. func (zi *ZettelIndex) AddDeadRef(zid id.Zid) { zi.deadrefs.Add(zid) } // SetWords sets the words to the given value. func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words } // SetUrls sets the words to the given value. func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls } // GetDeadRefs returns all dead references as a sorted list. func (zi *ZettelIndex) GetDeadRefs() *idset.Set { return zi.deadrefs } // GetMeta return just the raw metadata. func (zi *ZettelIndex) GetMeta() *meta.Meta { return zi.meta } // GetBackRefs returns all back references as a sorted list. func (zi *ZettelIndex) GetBackRefs() *idset.Set { return zi.backrefs } // GetInverseRefs returns all inverse meta references as a map of strings to a sorted list of references func (zi *ZettelIndex) GetInverseRefs() map[string]*idset.Set { return maps.Clone(zi.inverseRefs) } // GetWords returns a reference to the set of words. It must not be modified. func (zi *ZettelIndex) GetWords() WordSet { return zi.words } // GetUrls returns a reference to the set of URLs. It must not be modified. func (zi *ZettelIndex) GetUrls() WordSet { return zi.urls } |
Added internal/box/membox/membox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package membox stores zettel volatile in main memory. package membox import ( "context" "net/url" "sync" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) func init() { manager.Register( "mem", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &memBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "mem").Int("boxnum", int64(cdata.Number)).Child(), u: u, cdata: *cdata, maxZettel: box.GetQueryInt(u, "max-zettel", 0, 127, 65535), maxBytes: box.GetQueryInt(u, "max-bytes", 0, 65535, (1024*1024*1024)-1), }, nil }) } type memBox struct { log *logger.Logger u *url.URL cdata manager.ConnectData maxZettel int maxBytes int mx sync.RWMutex // Protects the following fields zettel map[id.Zid]zettel.Zettel curBytes int } func (mb *memBox) notifyChanged(zid id.Zid, reason box.UpdateReason) { if notify := mb.cdata.Notify; notify != nil { notify(mb, zid, reason) } } func (mb *memBox) Location() string { return mb.u.String() } func (mb *memBox) State() box.StartState { mb.mx.RLock() defer mb.mx.RUnlock() if mb.zettel == nil { return box.StartStateStopped } return box.StartStateStarted } func (mb *memBox) Start(context.Context) error { mb.mx.Lock() mb.zettel = make(map[id.Zid]zettel.Zettel) mb.curBytes = 0 mb.mx.Unlock() mb.log.Trace().Int("max-zettel", int64(mb.maxZettel)).Int("max-bytes", int64(mb.maxBytes)).Msg("Start Box") return nil } func (mb *memBox) Stop(context.Context) { mb.mx.Lock() mb.zettel = nil mb.mx.Unlock() } func (mb *memBox) CanCreateZettel(context.Context) bool { mb.mx.RLock() defer mb.mx.RUnlock() return len(mb.zettel) < mb.maxZettel } func (mb *memBox) CreateZettel(_ context.Context, zettel zettel.Zettel) (id.Zid, error) { mb.mx.Lock() newBytes := mb.curBytes + zettel.ByteSize() if mb.maxZettel < len(mb.zettel) || mb.maxBytes < newBytes { mb.mx.Unlock() return id.Invalid, box.ErrCapacity } zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { _, ok := mb.zettel[zid] return !ok, nil }) if err != nil { mb.mx.Unlock() return id.Invalid, err } meta := zettel.Meta.Clone() meta.Zid = zid zettel.Meta = meta mb.zettel[zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() mb.notifyChanged(zid, box.OnZettel) mb.log.Trace().Zid(zid).Msg("CreateZettel") return zid, nil } func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { mb.mx.RLock() z, ok := mb.zettel[zid] mb.mx.RUnlock() if !ok { return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } z.Meta = z.Meta.Clone() mb.log.Trace().Msg("GetZettel") return z, nil } func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool { mb.mx.RLock() _, found := mb.zettel[zid] mb.mx.RUnlock() return found } func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyZid") for zid := range mb.zettel { if constraint(zid) { handle(zid) } } return nil } func (mb *memBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyMeta") for zid, zettel := range mb.zettel { if constraint(zid) { m := zettel.Meta.Clone() mb.cdata.Enricher.Enrich(ctx, m, mb.cdata.Number) handle(m) } } return nil } func (mb *memBox) CanUpdateZettel(_ context.Context, zettel zettel.Zettel) bool { mb.mx.RLock() defer mb.mx.RUnlock() zid := zettel.Meta.Zid if !zid.IsValid() { return false } newBytes := mb.curBytes + zettel.ByteSize() if prevZettel, found := mb.zettel[zid]; found { newBytes -= prevZettel.ByteSize() } return newBytes < mb.maxBytes } func (mb *memBox) UpdateZettel(_ context.Context, zettel zettel.Zettel) error { m := zettel.Meta.Clone() if !m.Zid.IsValid() { return box.ErrInvalidZid{Zid: m.Zid.String()} } mb.mx.Lock() newBytes := mb.curBytes + zettel.ByteSize() if prevZettel, found := mb.zettel[m.Zid]; found { newBytes -= prevZettel.ByteSize() } if mb.maxBytes < newBytes { mb.mx.Unlock() return box.ErrCapacity } zettel.Meta = m mb.zettel[m.Zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() mb.notifyChanged(m.Zid, box.OnZettel) mb.log.Trace().Msg("UpdateZettel") return nil } func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { mb.mx.RLock() _, ok := mb.zettel[zid] mb.mx.RUnlock() return ok } func (mb *memBox) DeleteZettel(_ context.Context, zid id.Zid) error { mb.mx.Lock() oldZettel, found := mb.zettel[zid] if !found { mb.mx.Unlock() return box.ErrZettelNotFound{Zid: zid} } delete(mb.zettel, zid) mb.curBytes -= oldZettel.ByteSize() mb.mx.Unlock() mb.notifyChanged(zid, box.OnDelete) mb.log.Trace().Msg("DeleteZettel") return nil } func (mb *memBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = false mb.mx.RLock() st.Zettel = len(mb.zettel) mb.mx.RUnlock() mb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } |
Added internal/box/notify/directory.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "errors" "fmt" "path/filepath" "regexp" "slices" "sync" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/query" ) type entrySet map[id.Zid]*DirEntry // DirServiceState signal the internal state of the service. // // The following state transitions are possible: // --newDirService--> dsCreated // dsCreated --Start--> dsStarting // dsStarting --last list notification--> dsWorking // dsWorking --directory missing--> dsMissing // dsMissing --last list notification--> dsWorking // --Stop--> dsStopping type DirServiceState uint8 // Constants for DirServiceState const ( DsCreated DirServiceState = iota DsStarting // Reading inital scan DsWorking // Initial scan complete, fully operational DsMissing // Directory is missing DsStopping // Service is shut down ) // DirService specifies a directory service for file based zettel. type DirService struct { box box.ManagedBox log *logger.Logger dirPath string notifier Notifier infos box.UpdateNotifier mx sync.RWMutex // protects status, entries state DirServiceState entries entrySet } // ErrNoDirectory signals missing directory data. var ErrNoDirectory = errors.New("unable to retrieve zettel directory information") // NewDirService creates a new directory service. func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, notify box.UpdateNotifier) *DirService { return &DirService{ box: box, log: log, notifier: notifier, infos: notify, state: DsCreated, } } // State the current service state. func (ds *DirService) State() DirServiceState { ds.mx.RLock() state := ds.state ds.mx.RUnlock() return state } // Start the directory service. func (ds *DirService) Start() { ds.mx.Lock() ds.state = DsStarting ds.mx.Unlock() var newEntries entrySet go ds.updateEvents(newEntries) } // Refresh the directory entries. func (ds *DirService) Refresh() { ds.notifier.Refresh() } // Stop the directory service. func (ds *DirService) Stop() { ds.mx.Lock() ds.state = DsStopping ds.mx.Unlock() ds.notifier.Close() } func (ds *DirService) logMissingEntry(action string) error { err := ErrNoDirectory ds.log.Info().Err(err).Str("action", action).Msg("Unable to get directory information") return err } // NumDirEntries returns the number of entries in the directory. func (ds *DirService) NumDirEntries() int { ds.mx.RLock() defer ds.mx.RUnlock() if ds.entries == nil { return 0 } return len(ds.entries) } // GetDirEntries returns a list of directory entries, which satisfy the given constraint. func (ds *DirService) GetDirEntries(constraint query.RetrievePredicate) []*DirEntry { ds.mx.RLock() defer ds.mx.RUnlock() if ds.entries == nil { return nil } result := make([]*DirEntry, 0, len(ds.entries)) for zid, entry := range ds.entries { if constraint(zid) { copiedEntry := *entry result = append(result, &copiedEntry) } } return result } // GetDirEntry returns a directory entry with the given zid, or nil if not found. func (ds *DirService) GetDirEntry(zid id.Zid) *DirEntry { ds.mx.RLock() defer ds.mx.RUnlock() if ds.entries == nil { return nil } foundEntry := ds.entries[zid] if foundEntry == nil { return nil } result := *foundEntry return &result } // SetNewDirEntry calculates an empty directory entry with an unused identifier and // stores it in the directory. func (ds *DirService) SetNewDirEntry() (id.Zid, error) { ds.mx.Lock() defer ds.mx.Unlock() if ds.entries == nil { return id.Invalid, ds.logMissingEntry("new") } zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { _, found := ds.entries[zid] return !found, nil }) if err != nil { return id.Invalid, err } ds.entries[zid] = &DirEntry{Zid: zid} return zid, nil } // UpdateDirEntry updates an directory entry in place. func (ds *DirService) UpdateDirEntry(updatedEntry *DirEntry) error { entry := *updatedEntry ds.mx.Lock() defer ds.mx.Unlock() if ds.entries == nil { return ds.logMissingEntry("update") } ds.entries[entry.Zid] = &entry return nil } // DeleteDirEntry removes a entry from the directory. func (ds *DirService) DeleteDirEntry(zid id.Zid) error { ds.mx.Lock() defer ds.mx.Unlock() if ds.entries == nil { return ds.logMissingEntry("delete") } delete(ds.entries, zid) return nil } func (ds *DirService) updateEvents(newEntries entrySet) { // Something may panic. Ensure a running service. defer func() { if ri := recover(); ri != nil { kernel.Main.LogRecover("DirectoryService", ri) go ds.updateEvents(newEntries) } }() for ev := range ds.notifier.Events() { e, ok := ds.handleEvent(ev, newEntries) if !ok { break } newEntries = e } } func (ds *DirService) handleEvent(ev Event, newEntries entrySet) (entrySet, bool) { ds.mx.RLock() state := ds.state ds.mx.RUnlock() if msg := ds.log.Trace(); msg.Enabled() { msg.Uint("state", uint64(state)).Str("op", ev.Op.String()).Str("name", ev.Name).Msg("notifyEvent") } if state == DsStopping { return nil, false } switch ev.Op { case Error: newEntries = nil if state != DsMissing { ds.log.Error().Err(ev.Err).Msg("Notifier confused") } case Make: newEntries = make(entrySet) case List: if ev.Name == "" { zids := getNewZids(newEntries) ds.mx.Lock() fromMissing := ds.state == DsMissing prevEntries := ds.entries ds.entries = newEntries ds.state = DsWorking ds.mx.Unlock() ds.onCreateDirectory(zids, prevEntries) if fromMissing { ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found") } return nil, true } if newEntries != nil { ds.onUpdateFileEvent(newEntries, ev.Name) } case Destroy: ds.onDestroyDirectory() ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing") return nil, true case Update: ds.mx.Lock() zid := ds.onUpdateFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { ds.notifyChange(zid, box.OnZettel) } case Delete: ds.mx.Lock() zid := ds.onDeleteFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { ds.notifyChange(zid, box.OnDelete) } default: ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") } return newEntries, true } func getNewZids(entries entrySet) []id.Zid { zids := make([]id.Zid, 0, len(entries)) for zid := range entries { zids = append(zids, zid) } return zids } func (ds *DirService) onCreateDirectory(zids []id.Zid, prevEntries entrySet) { for _, zid := range zids { ds.notifyChange(zid, box.OnZettel) delete(prevEntries, zid) } // These were previously stored, by are not found now. // Notify system that these were deleted, e.g. for updating the index. for zid := range prevEntries { ds.notifyChange(zid, box.OnDelete) } } func (ds *DirService) onDestroyDirectory() { ds.mx.Lock() entries := ds.entries ds.entries = nil ds.state = DsMissing ds.mx.Unlock() for zid := range entries { ds.notifyChange(zid, box.OnDelete) } } var validFileName = regexp.MustCompile(`^(\d{14})`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } func seekZid(name string) id.Zid { match := matchValidFileName(name) if len(match) == 0 { return id.Invalid } zid, err := id.Parse(match[1]) if err != nil { return id.Invalid } return zid } func fetchdirEntry(entries entrySet, zid id.Zid) *DirEntry { if entry, found := entries[zid]; found { return entry } entry := &DirEntry{Zid: zid} entries[zid] = entry return entry } func (ds *DirService) onUpdateFileEvent(entries entrySet, name string) id.Zid { if entries == nil { return id.Invalid } zid := seekZid(name) if zid == id.Invalid { return id.Invalid } entry := fetchdirEntry(entries, zid) dupName1, dupName2 := ds.updateEntry(entry, name) if dupName1 != "" { ds.log.Info().Str("name", dupName1).Msg("Duplicate content (is ignored)") if dupName2 != "" { ds.log.Info().Str("name", dupName2).Msg("Duplicate content (is ignored)") } return id.Invalid } return zid } func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) id.Zid { if entries == nil { return id.Invalid } zid := seekZid(name) if zid == id.Invalid { return id.Invalid } entry, found := entries[zid] if !found { return zid } for i, dupName := range entry.UselessFiles { if dupName == name { removeDuplicate(entry, i) return zid } } if name == entry.ContentName { entry.ContentName = "" entry.ContentExt = "" ds.replayUpdateUselessFiles(entry) } else if name == entry.MetaName { entry.MetaName = "" ds.replayUpdateUselessFiles(entry) } if entry.ContentName == "" && entry.MetaName == "" { delete(entries, zid) } return zid } func removeDuplicate(entry *DirEntry, i int) { if len(entry.UselessFiles) == 1 { entry.UselessFiles = nil return } entry.UselessFiles = entry.UselessFiles[:i+copy(entry.UselessFiles[i:], entry.UselessFiles[i+1:])] } func (ds *DirService) replayUpdateUselessFiles(entry *DirEntry) { uselessFiles := entry.UselessFiles if len(uselessFiles) == 0 { return } entry.UselessFiles = make([]string, 0, len(uselessFiles)) for _, name := range uselessFiles { ds.updateEntry(entry, name) } if len(uselessFiles) == len(entry.UselessFiles) { return } loop: for _, prevName := range uselessFiles { for _, newName := range entry.UselessFiles { if prevName == newName { continue loop } } ds.log.Info().Str("name", prevName).Msg("Previous duplicate file becomes useful") } } func (ds *DirService) updateEntry(entry *DirEntry, name string) (string, string) { ext := onlyExt(name) if !extIsMetaAndContent(entry.ContentExt) { if ext == "" { return updateEntryMeta(entry, name), "" } if entry.MetaName == "" { if nameWithoutExt(name, ext) == entry.ContentName { // We have marked a file as content file, but it is a metadata file, // because it is the same as the new file without extension. entry.MetaName = entry.ContentName entry.ContentName = "" entry.ContentExt = "" ds.replayUpdateUselessFiles(entry) } else if entry.ContentName != "" && nameWithoutExt(entry.ContentName, entry.ContentExt) == name { // We have already a valid content file, and new file should serve as metadata file, // because it is the same as the content file without extension. entry.MetaName = name return "", "" } } } return updateEntryContent(entry, name, ext) } func nameWithoutExt(name, ext string) string { return name[0 : len(name)-len(ext)-1] } func updateEntryMeta(entry *DirEntry, name string) string { metaName := entry.MetaName if metaName == "" { entry.MetaName = name return "" } if metaName == name { return "" } if newNameIsBetter(metaName, name) { entry.MetaName = name return addUselessFile(entry, metaName) } return addUselessFile(entry, name) } func updateEntryContent(entry *DirEntry, name, ext string) (string, string) { contentName := entry.ContentName if contentName == "" { entry.ContentName = name entry.ContentExt = ext return "", "" } if contentName == name { return "", "" } contentExt := entry.ContentExt if contentExt == ext { if newNameIsBetter(contentName, name) { entry.ContentName = name return addUselessFile(entry, contentName), "" } return addUselessFile(entry, name), "" } if contentExt == extZettel { return addUselessFile(entry, name), "" } if ext == extZettel { entry.ContentName = name entry.ContentExt = ext contentName = addUselessFile(entry, contentName) if metaName := entry.MetaName; metaName != "" { metaName = addUselessFile(entry, metaName) entry.MetaName = "" return contentName, metaName } return contentName, "" } if newExtIsBetter(contentExt, ext) { entry.ContentName = name entry.ContentExt = ext return addUselessFile(entry, contentName), "" } return addUselessFile(entry, name), "" } func addUselessFile(entry *DirEntry, name string) string { if slices.Contains(entry.UselessFiles, name) { return "" } entry.UselessFiles = append(entry.UselessFiles, name) return name } func onlyExt(name string) string { ext := filepath.Ext(name) if ext == "" || ext[0] != '.' { return ext } return ext[1:] } func newNameIsBetter(oldName, newName string) bool { if len(oldName) < len(newName) { return false } return oldName > newName } var supportedSyntax, primarySyntax *set.Set[string] func init() { syntaxList := parser.GetSyntaxes() supportedSyntax = set.New(syntaxList...) primarySyntax = set.New[string]() for _, syntax := range syntaxList { if parser.Get(syntax).Name == syntax { primarySyntax.Add(syntax) } } } func newExtIsBetter(oldExt, newExt string) bool { oldSyntax := supportedSyntax.Contains(oldExt) if oldSyntax != supportedSyntax.Contains(newExt) { return !oldSyntax } if oldSyntax { if oldExt == "zmk" { return false } if newExt == "zmk" { return true } oldInfo := parser.Get(oldExt) newInfo := parser.Get(newExt) if oldASTParser := oldInfo.IsASTParser; oldASTParser != newInfo.IsASTParser { return !oldASTParser } if oldTextFormat := oldInfo.IsTextFormat; oldTextFormat != newInfo.IsTextFormat { return !oldTextFormat } if oldImageFormat := oldInfo.IsImageFormat; oldImageFormat != newInfo.IsImageFormat { return oldImageFormat } if oldPrimary := primarySyntax.Contains(oldExt); oldPrimary != primarySyntax.Contains(newExt) { return !oldPrimary } } oldLen := len(oldExt) newLen := len(newExt) if oldLen != newLen { return newLen < oldLen } return newExt < oldExt } func (ds *DirService) notifyChange(zid id.Zid, reason box.UpdateReason) { if notify := ds.infos; notify != nil { ds.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChange") notify(ds.box, zid, reason) } } |
Added internal/box/notify/directory_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "testing" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" ) func TestSeekZid(t *testing.T) { testcases := []struct { name string zid id.Zid }{ {"", id.Invalid}, {"1", id.Invalid}, {"1234567890123", id.Invalid}, {" 12345678901234", id.Invalid}, {"12345678901234", id.Zid(12345678901234)}, {"12345678901234.ext", id.Zid(12345678901234)}, {"12345678901234 abc.ext", id.Zid(12345678901234)}, {"12345678901234.abc.ext", id.Zid(12345678901234)}, {"12345678901234 def", id.Zid(12345678901234)}, } for _, tc := range testcases { gotZid := seekZid(tc.name) if gotZid != tc.zid { t.Errorf("seekZid(%q) == %v, but got %v", tc.name, tc.zid, gotZid) } } } func TestNewExtIsBetter(t *testing.T) { extVals := []string{ // Main Formats meta.ValueSyntaxZmk, meta.ValueSyntaxDraw, meta.ValueSyntaxMarkdown, meta.ValueSyntaxMD, // Other supported text formats meta.ValueSyntaxCSS, meta.ValueSyntaxSxn, meta.ValueSyntaxTxt, meta.ValueSyntaxHTML, meta.ValueSyntaxText, meta.ValueSyntaxPlain, // Supported text graphics formats meta.ValueSyntaxSVG, meta.ValueSyntaxNone, // Supported binary graphic formats meta.ValueSyntaxGif, meta.ValueSyntaxPNG, meta.ValueSyntaxJPEG, meta.ValueSyntaxWebp, meta.ValueSyntaxJPG, // Unsupported syntax values "gz", "cpp", "tar", "cppc", } for oldI, oldExt := range extVals { for newI, newExt := range extVals { if oldI <= newI { continue } if !newExtIsBetter(oldExt, newExt) { t.Errorf("newExtIsBetter(%q, %q) == true, but got false", oldExt, newExt) } if newExtIsBetter(newExt, oldExt) { t.Errorf("newExtIsBetter(%q, %q) == false, but got true", newExt, oldExt) } } } } |
Added internal/box/notify/entry.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "path/filepath" "slices" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/zettel" ) const ( extZettel = "zettel" // file contains metadata and content extBin = "bin" // file contains binary content extTxt = "txt" // file contains non-binary content ) func extIsMetaAndContent(ext string) bool { return ext == extZettel } // DirEntry stores everything for a directory entry. type DirEntry struct { Zid id.Zid MetaName string // file name of meta information ContentName string // file name of zettel content ContentExt string // (normalized) file extension of zettel content UselessFiles []string // list of other content files } // IsValid checks whether the entry is valid. func (e *DirEntry) IsValid() bool { return e != nil && e.Zid.IsValid() } // HasMetaInContent returns true, if metadata will be stored in the content file. func (e *DirEntry) HasMetaInContent() bool { return e.IsValid() && extIsMetaAndContent(e.ContentExt) } // SetupFromMetaContent fills entry data based on metadata and zettel content. func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content zettel.Content, getZettelFileSyntax func() []meta.Value) { if e.Zid != m.Zid { panic("Zid differ") } if contentName := e.ContentName; contentName != "" { if !extIsMetaAndContent(e.ContentExt) && e.MetaName == "" { e.MetaName = e.calcBaseName(contentName) } return } syntax := m.GetDefault(meta.KeySyntax, meta.DefaultSyntax) ext := calcContentExt(syntax, m.YamlSep, getZettelFileSyntax) metaName := e.MetaName eimc := extIsMetaAndContent(ext) if eimc { if metaName != "" { ext = contentExtWithMeta(syntax, content) } e.ContentName = e.calcBaseName(metaName) + "." + ext e.ContentExt = ext } else { if len(content.AsBytes()) > 0 { e.ContentName = e.calcBaseName(metaName) + "." + ext e.ContentExt = ext } if metaName == "" { e.MetaName = e.calcBaseName(e.ContentName) } } } func contentExtWithMeta(syntax meta.Value, content zettel.Content) string { p := parser.Get(string(syntax)) if content.IsBinary() { if p.IsImageFormat { return string(syntax) } return extBin } if p.IsImageFormat { return extTxt } return string(syntax) } func calcContentExt(syntax meta.Value, yamlSep bool, getZettelFileSyntax func() []meta.Value) string { if yamlSep { return extZettel } switch syntax { case meta.ValueSyntaxNone, meta.ValueSyntaxZmk: return extZettel } if slices.Contains(getZettelFileSyntax(), syntax) { return extZettel } return string(syntax) } func (e *DirEntry) calcBaseName(name string) string { if name == "" { return e.Zid.String() } return name[0 : len(name)-len(filepath.Ext(name))] } |
Added internal/box/notify/fsdir.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "os" "path/filepath" "strings" "github.com/fsnotify/fsnotify" "zettelstore.de/z/internal/logger" ) type fsdirNotifier struct { log *logger.Logger events chan Event done chan struct{} refresh chan struct{} base *fsnotify.Watcher path string fetcher EntryFetcher parent string } // NewFSDirNotifier creates a directory based notifier that receives notifications // from the file system. func NewFSDirNotifier(log *logger.Logger, path string) (Notifier, error) { absPath, err := filepath.Abs(path) if err != nil { log.Debug().Err(err).Str("path", path).Msg("Unable to create absolute path") return nil, err } watcher, err := fsnotify.NewWatcher() if err != nil { log.Debug().Err(err).Str("absPath", absPath).Msg("Unable to create watcher") return nil, err } absParentDir := filepath.Dir(absPath) errParent := watcher.Add(absParentDir) err = watcher.Add(absPath) if errParent != nil { if err != nil { log.Error(). Str("parentDir", absParentDir).Err(errParent). Str("path", absPath).Err(err). Msg("Unable to access Zettel directory and its parent directory") watcher.Close() return nil, err } log.Info().Str("parentDir", absParentDir).Err(errParent). Msg("Parent of Zettel directory cannot be supervised") log.Info().Str("path", absPath). Msg("Zettelstore might not detect a deletion or movement of the Zettel directory") } else if err != nil { // Not a problem, if container is not available. It might become available later. log.Info().Err(err).Str("path", absPath).Msg("Zettel directory currently not available") } fsdn := &fsdirNotifier{ log: log, events: make(chan Event), refresh: make(chan struct{}), done: make(chan struct{}), base: watcher, path: absPath, fetcher: newDirPathFetcher(absPath), parent: absParentDir, } go fsdn.eventLoop() return fsdn, nil } func (fsdn *fsdirNotifier) Events() <-chan Event { return fsdn.events } func (fsdn *fsdirNotifier) Refresh() { fsdn.refresh <- struct{}{} } func (fsdn *fsdirNotifier) eventLoop() { defer fsdn.base.Close() defer close(fsdn.events) defer close(fsdn.refresh) if !listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) { return } for fsdn.readAndProcessEvent() { } } func (fsdn *fsdirNotifier) readAndProcessEvent() bool { select { case <-fsdn.done: fsdn.traceDone(1) return false default: } select { case <-fsdn.done: fsdn.traceDone(2) return false case <-fsdn.refresh: fsdn.log.Trace().Msg("refresh") listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) case err, ok := <-fsdn.base.Errors: fsdn.log.Trace().Err(err).Bool("ok", ok).Msg("got errors") if !ok { return false } select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: fsdn.traceDone(3) return false } case ev, ok := <-fsdn.base.Events: fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Bool("ok", ok).Msg("file event") if !ok { return false } if !fsdn.processEvent(&ev) { return false } } return true } func (fsdn *fsdirNotifier) traceDone(pos int64) { fsdn.log.Trace().Int("i", pos).Msg("done with read and process events") } func (fsdn *fsdirNotifier) processEvent(ev *fsnotify.Event) bool { if strings.HasPrefix(ev.Name, fsdn.path) { if len(ev.Name) == len(fsdn.path) { return fsdn.processDirEvent(ev) } return fsdn.processFileEvent(ev) } fsdn.log.Trace().Str("path", fsdn.path).Str("name", ev.Name).Str("op", ev.Op.String()).Msg("event does not match") return true } func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool { if ev.Has(fsnotify.Remove) || ev.Has(fsnotify.Rename) { fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory removed") fsdn.base.Remove(fsdn.path) select { case fsdn.events <- Event{Op: Destroy}: case <-fsdn.done: fsdn.log.Trace().Int("i", 1).Msg("done dir event processing") return false } return true } if ev.Has(fsnotify.Create) { err := fsdn.base.Add(fsdn.path) if err != nil { fsdn.log.Error().Err(err).Str("name", fsdn.path).Msg("Unable to add directory") select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: fsdn.log.Trace().Int("i", 2).Msg("done dir event processing") return false } } fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory added") return listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) } fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("Directory processed") return true } func (fsdn *fsdirNotifier) processFileEvent(ev *fsnotify.Event) bool { if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) { if fi, err := os.Lstat(ev.Name); err != nil || !fi.Mode().IsRegular() { regular := err == nil && fi.Mode().IsRegular() fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Err(err).Bool("regular", regular).Msg("error with file") return true } fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated") return fsdn.sendEvent(Update, filepath.Base(ev.Name)) } if ev.Has(fsnotify.Rename) { fi, err := os.Lstat(ev.Name) if err != nil { fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted") return fsdn.sendEvent(Delete, filepath.Base(ev.Name)) } if fi.Mode().IsRegular() { fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated") return fsdn.sendEvent(Update, filepath.Base(ev.Name)) } fsdn.log.Trace().Str("name", ev.Name).Msg("File not regular") return true } if ev.Has(fsnotify.Remove) { fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted") return fsdn.sendEvent(Delete, filepath.Base(ev.Name)) } fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File processed") return true } func (fsdn *fsdirNotifier) sendEvent(op EventOp, filename string) bool { select { case fsdn.events <- Event{Op: op, Name: filename}: case <-fsdn.done: fsdn.log.Trace().Msg("done file event processing") return false } return true } func (fsdn *fsdirNotifier) Close() { close(fsdn.done) } |
Added internal/box/notify/helper.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "archive/zip" "os" "zettelstore.de/z/internal/logger" ) // EntryFetcher return a list of (file) names of an directory. type EntryFetcher interface { Fetch() ([]string, error) } type dirPathFetcher struct { dirPath string } func newDirPathFetcher(dirPath string) EntryFetcher { return &dirPathFetcher{dirPath} } func (dpf *dirPathFetcher) Fetch() ([]string, error) { entries, err := os.ReadDir(dpf.dirPath) if err != nil { return nil, err } result := make([]string, 0, len(entries)) for _, entry := range entries { if info, err1 := entry.Info(); err1 != nil || !info.Mode().IsRegular() { continue } result = append(result, entry.Name()) } return result, nil } type zipPathFetcher struct { zipPath string } func newZipPathFetcher(zipPath string) EntryFetcher { return &zipPathFetcher{zipPath} } func (zpf *zipPathFetcher) Fetch() ([]string, error) { reader, err := zip.OpenReader(zpf.zipPath) if err != nil { return nil, err } defer reader.Close() result := make([]string, 0, len(reader.File)) for _, f := range reader.File { result = append(result, f.Name) } return result, nil } // listDirElements write all files within the directory path as events. func listDirElements(log *logger.Logger, fetcher EntryFetcher, events chan<- Event, done <-chan struct{}) bool { select { case events <- Event{Op: Make}: case <-done: return false } entries, err := fetcher.Fetch() if err != nil { select { case events <- Event{Op: Error, Err: err}: case <-done: return false } } for _, name := range entries { log.Trace().Str("name", name).Msg("File listed") select { case events <- Event{Op: List, Name: name}: case <-done: return false } } select { case events <- Event{Op: List}: case <-done: return false } return true } |
Added internal/box/notify/notify.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package notify provides some notification services to be used by box services. package notify import "fmt" // Notifier send events about their container and content. type Notifier interface { // Return the channel Events() <-chan Event // Signal a refresh of the container. This will result in some events. Refresh() // Close the notifier (and eventually the channel) Close() } // EventOp describe a notification operation. type EventOp uint8 // Valid constants for event operations. // // Error signals a detected error. Details are in Event.Err. // // Make signals that the container is detected. List events will follow. // // List signals a found file, if Event.Name is not empty. Otherwise it signals // the end of files within the container. // // Destroy signals that the container is not there any more. It might me Make later again. // // Update signals that file Event.Name was created/updated. // File name is relative to the container. // // Delete signals that file Event.Name was removed. // File name is relative to the container's name. const ( _ EventOp = iota Error // Error while operating Make // Make container List // List container Destroy // Destroy container Update // Update element Delete // Delete element ) // String representation of operation code. func (c EventOp) String() string { switch c { case Error: return "ERROR" case Make: return "MAKE" case List: return "LIST" case Destroy: return "DESTROY" case Update: return "UPDATE" case Delete: return "DELETE" default: return fmt.Sprintf("UNKNOWN(%d)", c) } } // Event represents a single container / element event. type Event struct { Op EventOp Name string Err error // Valid iff Op == Error } |
Added internal/box/notify/simpledir.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "path/filepath" "zettelstore.de/z/internal/logger" ) type simpleDirNotifier struct { log *logger.Logger events chan Event done chan struct{} refresh chan struct{} fetcher EntryFetcher } // NewSimpleDirNotifier creates a directory based notifier that will not receive // any notifications from the operating system. func NewSimpleDirNotifier(log *logger.Logger, path string) (Notifier, error) { absPath, err := filepath.Abs(path) if err != nil { return nil, err } sdn := &simpleDirNotifier{ log: log, events: make(chan Event), done: make(chan struct{}), refresh: make(chan struct{}), fetcher: newDirPathFetcher(absPath), } go sdn.eventLoop() return sdn, nil } // NewSimpleZipNotifier creates a zip-file based notifier that will not receive // any notifications from the operating system. func NewSimpleZipNotifier(log *logger.Logger, zipPath string) Notifier { sdn := &simpleDirNotifier{ log: log, events: make(chan Event), done: make(chan struct{}), refresh: make(chan struct{}), fetcher: newZipPathFetcher(zipPath), } go sdn.eventLoop() return sdn } func (sdn *simpleDirNotifier) Events() <-chan Event { return sdn.events } func (sdn *simpleDirNotifier) Refresh() { sdn.refresh <- struct{}{} } func (sdn *simpleDirNotifier) eventLoop() { defer close(sdn.events) defer close(sdn.refresh) if !listDirElements(sdn.log, sdn.fetcher, sdn.events, sdn.done) { return } for { select { case <-sdn.done: return case <-sdn.refresh: listDirElements(sdn.log, sdn.fetcher, sdn.events, sdn.done) } } } func (sdn *simpleDirNotifier) Close() { close(sdn.done) } |
Added internal/collect/collect.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/internal/ast" // Summary stores the relevant parts of the syntax tree type Summary struct { Links []*ast.Reference // list of all linked material Embeds []*ast.Reference // list of all embedded material Cites []*ast.CiteNode // list of all referenced citations } // References returns all references mentioned in the given zettel. This also // includes references to images. func References(zn *ast.ZettelNode) (s Summary) { ast.Walk(&s, &zn.BlocksAST) return s } // Visit all node to collect data for the summary. func (s *Summary) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.TranscludeNode: s.Embeds = append(s.Embeds, n.Ref) case *ast.LinkNode: s.Links = append(s.Links, n.Ref) case *ast.EmbedRefNode: s.Embeds = append(s.Embeds, n.Ref) case *ast.CiteNode: s.Cites = append(s.Cites, n) } return s } |
Added internal/collect/collect_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package collect_test provides some unit test for collectors. package collect_test import ( "testing" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/collect" ) func parseRef(s string) *ast.Reference { r := ast.ParseReference(s) if !r.IsValid() { panic(s) } return r } func TestLinks(t *testing.T) { t.Parallel() zn := &ast.ZettelNode{} summary := collect.References(zn) if summary.Links != nil || summary.Embeds != nil { t.Error("No links/images expected, but got:", summary.Links, "and", summary.Embeds) } intNode := &ast.LinkNode{Ref: parseRef("01234567890123")} para := ast.CreateParaNode(intNode, &ast.LinkNode{Ref: parseRef("https://zettelstore.de/z")}) zn.BlocksAST = ast.BlockSlice{para} summary = collect.References(zn) if summary.Links == nil || summary.Embeds != nil { t.Error("Links expected, and no images, but got:", summary.Links, "and", summary.Embeds) } para.Inlines = append(para.Inlines, intNode) summary = collect.References(zn) if cnt := len(summary.Links); cnt != 3 { t.Error("Link count does not work. Expected: 3, got", summary.Links) } } func TestEmbed(t *testing.T) { t.Parallel() zn := &ast.ZettelNode{ BlocksAST: ast.BlockSlice{ast.CreateParaNode(&ast.EmbedRefNode{Ref: parseRef("12345678901234")})}, } summary := collect.References(zn) if summary.Embeds == nil { t.Error("Only image expected, but got: ", summary.Embeds) } } |
Added internal/collect/order.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/internal/ast" // Order of internal links within the given zettel. func Order(zn *ast.ZettelNode) (result []*ast.LinkNode) { for _, bn := range zn.BlocksAST { ln, ok := bn.(*ast.NestedListNode) if !ok { continue } switch ln.Kind { case ast.NestedListOrdered, ast.NestedListUnordered: for _, is := range ln.Items { if ln := firstItemZettelLink(is); ln != nil { result = append(result, ln) } } } } return result } func firstItemZettelLink(is ast.ItemSlice) *ast.LinkNode { for _, in := range is { if pn, ok := in.(*ast.ParaNode); ok { if ln := firstInlineZettelLink(pn.Inlines); ln != nil { return ln } } } return nil } func firstInlineZettelLink(is ast.InlineSlice) (result *ast.LinkNode) { for _, inl := range is { switch in := inl.(type) { case *ast.LinkNode: return in case *ast.EmbedRefNode: result = firstInlineZettelLink(in.Inlines) case *ast.EmbedBLOBNode: result = firstInlineZettelLink(in.Inlines) case *ast.CiteNode: result = firstInlineZettelLink(in.Inlines) case *ast.FootnoteNode: // Ignore references in footnotes continue case *ast.FormatNode: result = firstInlineZettelLink(in.Inlines) default: continue } if result != nil { return result } } return nil } |
Added internal/config/config.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package config provides functions to retrieve runtime configuration data. package config import ( "context" "t73f.de/r/zsc/domain/meta" ) // Key values that are supported by Config.Get const ( KeyFooterZettel = "footer-zettel" KeyHomeZettel = "home-zettel" KeyListsMenuZettel = "lists-menu-zettel" KeyShowBackLinks = "show-back-links" KeyShowFolgeLinks = "show-folge-links" KeyShowSequelLinks = "show-sequel-links" KeyShowSubordinateLinks = "show-subordinate-links" KeyShowSuccessorLinks = "show-successor-links" // api.KeyLang ) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig // Get returns the value of the given key. It searches first in the given metadata, // then in the data of the current user, and at last in the system-wide data. Get(ctx context.Context, m *meta.Meta, key string) string // AddDefaultValues enriches the given meta data with its default values. AddDefaultValues(context.Context, *meta.Meta) *meta.Meta // GetSiteName returns the current value of the "site-name" key. GetSiteName() string // GetHTMLInsecurity returns the current GetHTMLInsecurity() HTMLInsecurity // GetMaxTransclusions returns the maximum number of indirect transclusions. GetMaxTransclusions() int // GetYAMLHeader returns the current value of the "yaml-header" key. GetYAMLHeader() bool // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. GetZettelFileSyntax() []meta.Value } // AuthConfig are relevant configuration values for authentication. type AuthConfig interface { // GetSimpleMode returns true if system tuns in simple-mode. GetSimpleMode() bool // GetExpertMode returns the current value of the "expert-mode" key. GetExpertMode() bool // GetVisibility returns the visibility value of the metadata. GetVisibility(m *meta.Meta) meta.Visibility } // HTMLInsecurity states what kind of insecure HTML is allowed. // The lowest value is the most secure one (disallowing any HTML) type HTMLInsecurity uint8 // Constant values for HTMLInsecurity: const ( NoHTML HTMLInsecurity = iota SyntaxHTML MarkdownHTML ZettelmarkupHTML ) func (hi HTMLInsecurity) String() string { switch hi { case SyntaxHTML: return "html" case MarkdownHTML: return "markdown" case ZettelmarkupHTML: return "zettelmarkup" } return "secure" } // AllowHTML returns true, if the given HTML insecurity level matches the given syntax value. func (hi HTMLInsecurity) AllowHTML(syntax string) bool { switch hi { case SyntaxHTML: return syntax == meta.ValueSyntaxHTML case MarkdownHTML: return syntax == meta.ValueSyntaxHTML || syntax == meta.ValueSyntaxMarkdown || syntax == meta.ValueSyntaxMD case ZettelmarkupHTML: return syntax == meta.ValueSyntaxZmk || syntax == meta.ValueSyntaxHTML || syntax == meta.ValueSyntaxMarkdown || syntax == meta.ValueSyntaxMD } return false } |
Added internal/encoder/encoder.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package encoder provides a generic interface to encode the abstract syntax // tree into some text form. package encoder import ( "io" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" ) // Encoder is an interface that allows to encode different parts of a zettel. type Encoder interface { // WriteZettel encodes a whole zettel and writes it to the Writer. WriteZettel(io.Writer, *ast.ZettelNode) (int, error) // WriteMeta encodes just the metadata. WriteMeta(io.Writer, *meta.Meta) (int, error) // WiteBlocks encodes a block slice, i.e. the zettel content. WriteBlocks(io.Writer, *ast.BlockSlice) (int, error) } // Create builds a new encoder with the given options. func Create(enc api.EncodingEnum, params *CreateParameter) Encoder { switch enc { case api.EncoderHTML: // We need a new transformer every time, because tx.inVerse must be unique. // If we can refactor it out, the transformer can be created only once. return &htmlEncoder{ tx: NewSzTransformer(), th: shtml.NewEvaluator(1), lang: params.Lang, } case api.EncoderMD: return &mdEncoder{lang: params.Lang} case api.EncoderSHTML: // We need a new transformer every time, because tx.inVerse must be unique. // If we can refactor it out, the transformer can be created only once. return &shtmlEncoder{ tx: NewSzTransformer(), th: shtml.NewEvaluator(1), lang: params.Lang, } case api.EncoderSz: // We need a new transformer every time, because trans.inVerse must be unique. // If we can refactor it out, the transformer can be created only once. return &szEncoder{trans: NewSzTransformer()} case api.EncoderText: return (*TextEncoder)(nil) case api.EncoderZmk: return (*zmkEncoder)(nil) } return nil } // CreateParameter contains values that are needed to create some encoder. type CreateParameter struct { Lang string // default language } // GetEncodings returns all registered encodings, ordered by encoding value. func GetEncodings() []api.EncodingEnum { return []api.EncodingEnum{api.EncoderHTML, api.EncoderMD, api.EncoderSHTML, api.EncoderSz, api.EncoderText, api.EncoderZmk} } |
Added internal/encoder/encoder_blob_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package encoder_test import ( "testing" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/parser" ) type blobTestCase struct { descr string syntax string blob []byte expect expectMap } var pngTestCases = []blobTestCase{ { descr: "Minimal PNG", syntax: "png", blob: []byte{ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x7e, 0x9b, 0x55, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x62, 0x00, 0x00, 0x00, 0x06, 0x00, 0x03, 0x36, 0x37, 0x7c, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, }, expect: expectMap{ encoderHTML: `<p><img alt="Minimal PNG" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="></p>`, encoderSz: `(BLOCK (BLOB () ((TEXT "Minimal PNG")) "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`, encoderSHTML: `((p (img (@ (alt . "Minimal PNG") (src . "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==")))))`, encoderText: "", encoderZmk: `%% Unable to display BLOB with description 'Minimal PNG' and syntax 'png'.`, }, }, } func TestBlob(t *testing.T) { m := meta.New(id.Invalid) for testNum, tc := range pngTestCases { m.Set(meta.KeyTitle, meta.Value(tc.descr)) inp := input.NewInput(tc.blob) bs := parser.Parse(inp, m, tc.syntax, config.NoHTML) checkEncodings(t, testNum, bs, false, tc.descr, tc.expect, "???") } } |
Added internal/encoder/encoder_block_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package encoder_test var tcsBlock = []zmkTestCase{ { descr: "Empty Zettelmarkup should produce near nothing", zmk: "", expect: expectMap{ encoderHTML: "", encoderMD: "", encoderSz: `(BLOCK)`, encoderSHTML: `()`, encoderText: "", encoderZmk: useZmk, }, }, { descr: "Simple text: Hello, world", zmk: "Hello, world", expect: expectMap{ encoderHTML: "<p>Hello, world</p>", encoderMD: "Hello, world", encoderSz: `(BLOCK (PARA (TEXT "Hello, world")))`, encoderSHTML: `((p "Hello, world"))`, encoderText: "Hello, world", encoderZmk: useZmk, }, }, { descr: "Simple block comment", zmk: "%%%\nNo\nrender\n%%%", expect: expectMap{ encoderHTML: ``, encoderMD: "", encoderSz: `(BLOCK (VERBATIM-COMMENT () "No\nrender"))`, encoderSHTML: `(())`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Rendered block comment", zmk: "%%%{-}\nRender\n%%%", expect: expectMap{ encoderHTML: "<!--\nRender\n-->\n", encoderMD: "", encoderSz: `(BLOCK (VERBATIM-COMMENT (("-" . "")) "Render"))`, encoderSHTML: "((@@@ \"Render\"))", encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Simple Heading", zmk: `=== Top Job`, expect: expectMap{ encoderHTML: "<h2 id=\"top-job\">Top Job</h2>", encoderMD: "# Top Job", encoderSz: `(BLOCK (HEADING 1 () "top-job" "top-job" (TEXT "Top Job")))`, encoderSHTML: `((h2 (@ (id . "top-job")) "Top Job"))`, encoderText: `Top Job`, encoderZmk: useZmk, }, }, { descr: "Simple List", zmk: "* A\n* B\n* C", expect: expectMap{ encoderHTML: "<ul><li>A</li><li>B</li><li>C</li></ul>", encoderMD: "* A\n* B\n* C", encoderSz: `(BLOCK (UNORDERED () (INLINE (TEXT "A")) (INLINE (TEXT "B")) (INLINE (TEXT "C"))))`, encoderSHTML: `((ul (li "A") (li "B") (li "C")))`, encoderText: "A\nB\nC", encoderZmk: useZmk, }, }, { descr: "Nested List", zmk: "* T1\n*# T2\n* T3\n** T4\n** T5\n* T6", expect: expectMap{ encoderHTML: `<ul><li><p>T1</p><ol><li>T2</li></ol></li><li><p>T3</p><ul><li>T4</li><li>T5</li></ul></li><li><p>T6</p></li></ul>`, encoderMD: "* T1\n 1. T2\n* T3\n * T4\n * T5\n* T6", encoderSz: `(BLOCK (UNORDERED () (BLOCK (PARA (TEXT "T1")) (ORDERED () (INLINE (TEXT "T2")))) (BLOCK (PARA (TEXT "T3")) (UNORDERED () (INLINE (TEXT "T4")) (INLINE (TEXT "T5")))) (BLOCK (PARA (TEXT "T6")))))`, encoderSHTML: `((ul (li (p "T1") (ol (li "T2"))) (li (p "T3") (ul (li "T4") (li "T5"))) (li (p "T6"))))`, encoderText: "T1\nT2\nT3\nT4\nT5\nT6", encoderZmk: useZmk, }, }, { descr: "Sequence of two lists", zmk: "* Item1.1\n* Item1.2\n* Item1.3\n\n* Item2.1\n* Item2.2", expect: expectMap{ encoderHTML: "<ul><li>Item1.1</li><li>Item1.2</li><li>Item1.3</li><li>Item2.1</li><li>Item2.2</li></ul>", encoderMD: "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2", encoderSz: `(BLOCK (UNORDERED () (INLINE (TEXT "Item1.1")) (INLINE (TEXT "Item1.2")) (INLINE (TEXT "Item1.3")) (INLINE (TEXT "Item2.1")) (INLINE (TEXT "Item2.2"))))`, encoderSHTML: `((ul (li "Item1.1") (li "Item1.2") (li "Item1.3") (li "Item2.1") (li "Item2.2")))`, encoderText: "Item1.1\nItem1.2\nItem1.3\nItem2.1\nItem2.2", encoderZmk: "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2", }, }, { descr: "Simple horizontal rule", zmk: `---`, expect: expectMap{ encoderHTML: "<hr>", encoderMD: "---", encoderSz: `(BLOCK (THEMATIC ()))`, encoderSHTML: `((hr))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Thematic break with attribute", zmk: `---{lang="zmk"}`, expect: expectMap{ encoderHTML: `<hr lang="zmk">`, encoderMD: "---", encoderSz: `(BLOCK (THEMATIC (("lang" . "zmk"))))`, encoderSHTML: `((hr (@ (lang . "zmk"))))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "No list after paragraph", zmk: "Text\n*abc", expect: expectMap{ encoderHTML: "<p>Text *abc</p>", encoderMD: "Text\n*abc", encoderSz: `(BLOCK (PARA (TEXT "Text") (SOFT) (TEXT "*abc")))`, encoderSHTML: `((p "Text" " " "*abc"))`, encoderText: `Text *abc`, encoderZmk: useZmk, }, }, { descr: "A list after paragraph", zmk: "Text\n# abc", expect: expectMap{ encoderHTML: "<p>Text</p><ol><li>abc</li></ol>", encoderMD: "Text\n\n1. abc", encoderSz: `(BLOCK (PARA (TEXT "Text")) (ORDERED () (INLINE (TEXT "abc"))))`, encoderSHTML: `((p "Text") (ol (li "abc")))`, encoderText: "Text\nabc", encoderZmk: useZmk, }, }, { descr: "Simple List Quote", zmk: "> ToBeOrNotToBe", expect: expectMap{ encoderHTML: "<blockquote>ToBeOrNotToBe</blockquote>", encoderMD: "> ToBeOrNotToBe", encoderSz: `(BLOCK (QUOTATION () (INLINE (TEXT "ToBeOrNotToBe"))))`, encoderSHTML: `((blockquote (@L "ToBeOrNotToBe")))`, encoderText: "ToBeOrNotToBe", encoderZmk: useZmk, }, }, { descr: "Simple Quote Block", zmk: "<<<\nToBeOrNotToBe\n<<< Romeo Julia", expect: expectMap{ encoderHTML: "<blockquote><p>ToBeOrNotToBe</p><cite>Romeo Julia</cite></blockquote>", encoderMD: "> ToBeOrNotToBe", encoderSz: `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) (TEXT "Romeo Julia")))`, encoderSHTML: `((blockquote (p "ToBeOrNotToBe") (cite "Romeo Julia")))`, encoderText: "ToBeOrNotToBe\nRomeo Julia", encoderZmk: useZmk, }, }, { descr: "Quote Block with multiple paragraphs", zmk: "<<<\nToBeOr\n\nNotToBe\n<<< Romeo", expect: expectMap{ encoderHTML: "<blockquote><p>ToBeOr</p><p>NotToBe</p><cite>Romeo</cite></blockquote>", encoderMD: "> ToBeOr\n>\n> NotToBe", encoderSz: `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) (TEXT "Romeo")))`, encoderSHTML: `((blockquote (p "ToBeOr") (p "NotToBe") (cite "Romeo")))`, encoderText: "ToBeOr\nNotToBe\nRomeo", encoderZmk: useZmk, }, }, { descr: "Verse block", zmk: `""" A line another line Back Paragraph Spacy Para """ Author`, expect: expectMap{ encoderHTML: "<div><p>A\u00a0line<br>\u00a0\u00a0another\u00a0line<br>Back</p><p>Paragraph</p><p>\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para</p><cite>Author</cite></div>", encoderMD: "", encoderSz: "(BLOCK (REGION-VERSE () ((PARA (TEXT \"A\u00a0line\") (HARD) (TEXT \"\u00a0\u00a0another\u00a0line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (TEXT \"\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\"))) (TEXT \"Author\")))", encoderSHTML: "((div (p \"A\u00a0line\" (br) \"\u00a0\u00a0another\u00a0line\" (br) \"Back\") (p \"Paragraph\") (p \"\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\") (cite \"Author\")))", encoderText: "A line\n another line\nBack\nParagraph\n Spacy Para\nAuthor", encoderZmk: "\"\"\"\nA\u00a0line\\\n\u00a0\u00a0another\u00a0line\\\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\n\"\"\" Author", }, }, { descr: "Span Block", zmk: `::: A simple span and much more :::`, expect: expectMap{ encoderHTML: "<div><p>A simple span and much more</p></div>", encoderMD: "", encoderSz: `(BLOCK (REGION-BLOCK () ((PARA (TEXT "A simple") (SOFT) (TEXT " span") (SOFT) (TEXT "and much more")))))`, encoderSHTML: `((div (p "A simple" " " " span" " " "and much more")))`, encoderText: `A simple span and much more`, encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Code", zmk: "```\nHello\nWorld\n```", expect: expectMap{ encoderHTML: "<pre><code>Hello\nWorld</code></pre>", encoderMD: " Hello\n World", encoderSz: `(BLOCK (VERBATIM-CODE () "Hello\nWorld"))`, encoderSHTML: `((pre (code "Hello\nWorld")))`, encoderText: "Hello\nWorld", encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Code with visible spaces", zmk: "```{-}\nHello World\n```", expect: expectMap{ encoderHTML: "<pre><code>Hello\u2423World</code></pre>", encoderMD: " Hello World", encoderSz: `(BLOCK (VERBATIM-CODE (("-" . "")) "Hello World"))`, encoderSHTML: "((pre (code \"Hello\u2423World\")))", encoderText: "Hello World", encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Eval", zmk: "~~~\nHello\nWorld\n~~~", expect: expectMap{ encoderHTML: "<pre><code class=\"zs-eval\">Hello\nWorld</code></pre>", encoderMD: "", encoderSz: `(BLOCK (VERBATIM-EVAL () "Hello\nWorld"))`, encoderSHTML: "((pre (code (@ (class . \"zs-eval\")) \"Hello\\nWorld\")))", encoderText: "Hello\nWorld", encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Math", zmk: "$$$\nHello\n\\LaTeX\n$$$", expect: expectMap{ encoderHTML: "<pre><code class=\"zs-math\">Hello\n\\LaTeX</code></pre>", encoderMD: "", encoderSz: `(BLOCK (VERBATIM-MATH () "Hello\n\\LaTeX"))`, encoderSHTML: "((pre (code (@ (class . \"zs-math\")) \"Hello\\n\\\\LaTeX\")))", encoderText: "Hello\n\\LaTeX", encoderZmk: useZmk, }, }, { descr: "Simple Description List", zmk: "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box", expect: expectMap{ encoderHTML: "<dl><dt>Zettel</dt><dd><p>Paper</p></dd><dd><p>Note</p></dd><dt>Zettelkasten</dt><dd><p>Slip box</p></dd></dl>", encoderMD: "", encoderSz: `(BLOCK (DESCRIPTION () ((TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper"))) (BLOCK (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip box"))))))`, encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper")) (dd (p "Note")) (dt "Zettelkasten") (dd (p "Slip box"))))`, encoderText: "Zettel\nPaper\nNote\nZettelkasten\nSlip box", encoderZmk: useZmk, }, }, { descr: "Description List with paragraphs as item", zmk: "; Zettel\n: Paper\n\n Note\n; Zettelkasten\n: Slip box", expect: expectMap{ encoderHTML: "<dl><dt>Zettel</dt><dd><p>Paper</p><p>Note</p></dd><dt>Zettelkasten</dt><dd><p>Slip box</p></dd></dl>", encoderMD: "", encoderSz: `(BLOCK (DESCRIPTION () ((TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper")) (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip box"))))))`, encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper") (p "Note")) (dt "Zettelkasten") (dd (p "Slip box"))))`, encoderText: "Zettel\nPaper\nNote\nZettelkasten\nSlip box", encoderZmk: useZmk, }, }, { descr: "Description List with keys, but no descriptions", zmk: "; K1\n: D11\n: D12\n; K2\n; K3\n: D31", expect: expectMap{ encoderHTML: "<dl><dt>K1</dt><dd><p>D11</p></dd><dd><p>D12</p></dd><dt>K2</dt><dt>K3</dt><dd><p>D31</p></dd></dl>", encoderMD: "", encoderSz: `(BLOCK (DESCRIPTION () ((TEXT "K1")) (BLOCK (BLOCK (PARA (TEXT "D11"))) (BLOCK (PARA (TEXT "D12")))) ((TEXT "K2")) (BLOCK) ((TEXT "K3")) (BLOCK (BLOCK (PARA (TEXT "D31"))))))`, encoderSHTML: `((dl (dt "K1") (dd (p "D11")) (dd (p "D12")) (dt "K2") (dt "K3") (dd (p "D31"))))`, encoderText: "K1\nD11\nD12\nK2\nK3\nD31", encoderZmk: useZmk, }, }, { descr: "Simple Table", zmk: "|c1|c2|c3\n|d1||d3", expect: expectMap{ encoderHTML: `<table><tbody><tr><td>c1</td><td>c2</td><td>c3</td></tr><tr><td>d1</td><td></td><td>d3</td></tr></tbody></table>`, encoderMD: "", encoderSz: `(BLOCK (TABLE () ((CELL () (TEXT "c1")) (CELL () (TEXT "c2")) (CELL () (TEXT "c3"))) ((CELL () (TEXT "d1")) (CELL ()) (CELL () (TEXT "d3")))))`, encoderSHTML: `((table (tbody (tr (td "c1") (td "c2") (td "c3")) (tr (td "d1") (td) (td "d3")))))`, encoderText: "c1 c2 c3\nd1 d3", encoderZmk: useZmk, }, }, { descr: "Table with alignment and comment", zmk: `|h1>|=h2|h3:| |%--+---+---+ |<c1|c2|:c3| |f1|f2|=f3`, expect: expectMap{ encoderHTML: `<table><thead><tr><th class="right">h1</th><th>h2</th><th class="center">h3</th></tr></thead><tbody><tr><td class="left">c1</td><td>c2</td><td class="center">c3</td></tr><tr><td class="right">f1</td><td>f2</td><td class="center">=f3</td></tr></tbody></table>`, encoderMD: "", encoderSz: `(BLOCK (TABLE ((CELL ((align . "right")) (TEXT "h1")) (CELL () (TEXT "h2")) (CELL ((align . "center")) (TEXT "h3"))) ((CELL ((align . "left")) (TEXT "c1")) (CELL () (TEXT "c2")) (CELL ((align . "center")) (TEXT "c3"))) ((CELL ((align . "right")) (TEXT "f1")) (CELL () (TEXT "f2")) (CELL ((align . "center")) (TEXT "=f3")))))`, encoderSHTML: `((table (thead (tr (th (@ (class . "right")) "h1") (th "h2") (th (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`, encoderText: "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3", encoderZmk: /*`|=h1>|=h2|=h3: |<c1|c2|c3 |f1|f2|=f3`,*/ `|=>h1|=h2|=:h3 |<c1|c2|:c3 |>f1|f2|:=f3`, }, }, { descr: "Simple Endnote", zmk: `Text[^Endnote]`, expect: expectMap{ encoderHTML: "<p>Text<sup id=\"fnref:1\"><a class=\"zs-noteref\" href=\"#fn:1\" role=\"doc-noteref\">1</a></sup></p><ol class=\"zs-endnotes\"><li class=\"zs-endnote\" id=\"fn:1\" role=\"doc-endnote\" value=\"1\">Endnote <a class=\"zs-endnote-backref\" href=\"#fnref:1\" role=\"doc-backlink\">\u21a9\ufe0e</a></li></ol>", encoderMD: "Text", encoderSz: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (TEXT "Endnote"))))`, encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))", encoderText: "Text Endnote", encoderZmk: useZmk, }, }, { descr: "Nested Endnotes", zmk: `Text[^Endnote[^Nested]]`, expect: expectMap{ encoderHTML: "<p>Text<sup id=\"fnref:1\"><a class=\"zs-noteref\" href=\"#fn:1\" role=\"doc-noteref\">1</a></sup></p><ol class=\"zs-endnotes\"><li class=\"zs-endnote\" id=\"fn:1\" role=\"doc-endnote\" value=\"1\">Endnote<sup id=\"fnref:2\"><a class=\"zs-noteref\" href=\"#fn:2\" role=\"doc-noteref\">2</a></sup> <a class=\"zs-endnote-backref\" href=\"#fnref:1\" role=\"doc-backlink\">\u21a9\ufe0e</a></li><li class=\"zs-endnote\" id=\"fn:2\" role=\"doc-endnote\" value=\"2\">Nested <a class=\"zs-endnote-backref\" href=\"#fnref:2\" role=\"doc-backlink\">\u21a9\ufe0e</a></li></ol>", encoderMD: "Text", encoderSz: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (TEXT "Endnote") (ENDNOTE () (TEXT "Nested")))))`, encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))", encoderText: "Text Endnote Nested", encoderZmk: useZmk, }, }, { descr: "Transclusion", zmk: `{{{http://example.com/image}}}{width="100px"}`, expect: expectMap{ encoderHTML: `<p><img class="external" src="http://example.com/image" width="100px"></p>`, encoderMD: "", encoderSz: `(BLOCK (TRANSCLUDE (("width" . "100px")) (EXTERNAL "http://example.com/image")))`, encoderSHTML: `((p (img (@ (class . "external") (src . "http://example.com/image") (width . "100px")))))`, encoderText: "", encoderZmk: useZmk, }, }, { descr: "A paragraph with a inline comment only should be empty in HTML", zmk: `%% Comment`, expect: expectMap{ // encoderHTML: ``, encoderSz: `(BLOCK (PARA (LITERAL-COMMENT () "Comment")))`, // encoderSHTML: ``, encoderText: "", encoderZmk: useZmk, }, }, { descr: "", zmk: ``, expect: expectMap{ encoderHTML: ``, encoderSz: `(BLOCK)`, encoderSHTML: `()`, encoderText: "", encoderZmk: useZmk, }, }, } // func TestEncoderBlock(t *testing.T) { // executeTestCases(t, tcsBlock) // } |
Added internal/encoder/encoder_inline_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package encoder_test var tcsInline = []zmkTestCase{ { descr: "Empty Zettelmarkup should produce near nothing (inline)", zmk: "", expect: expectMap{ encoderHTML: "", encoderMD: "", encoderSz: `(BLOCK)`, encoderSHTML: `()`, encoderText: "", encoderZmk: useZmk, }, }, { descr: "Simple text: Hello, world (inline)", zmk: `Hello, world`, expect: expectMap{ encoderHTML: "<p>Hello, world</p>", encoderMD: "Hello, world", encoderSz: `(BLOCK (PARA (TEXT "Hello, world")))`, encoderSHTML: `((p "Hello, world"))`, encoderText: "Hello, world", encoderZmk: useZmk, }, }, { descr: "Soft Break", zmk: "soft\nbreak", expect: expectMap{ encoderHTML: "<p>soft break</p>", encoderMD: "soft\nbreak", encoderSz: `(BLOCK (PARA (TEXT "soft") (SOFT) (TEXT "break")))`, encoderSHTML: `((p "soft" " " "break"))`, encoderText: "soft break", encoderZmk: useZmk, }, }, { descr: "Hard Break", zmk: "hard\\\nbreak", expect: expectMap{ encoderHTML: "<p>hard<br>break</p>", encoderMD: "hard\\\nbreak", encoderSz: `(BLOCK (PARA (TEXT "hard") (HARD) (TEXT "break")))`, encoderSHTML: `((p "hard" (br) "break"))`, encoderText: "hard\nbreak", encoderZmk: useZmk, }, }, { descr: "Emphasized formatting", zmk: "__emph__", expect: expectMap{ encoderHTML: "<p><em>emph</em></p>", encoderMD: "*emph*", encoderSz: `(BLOCK (PARA (FORMAT-EMPH () (TEXT "emph"))))`, encoderSHTML: `((p (em "emph")))`, encoderText: "emph", encoderZmk: useZmk, }, }, { descr: "Strong formatting", zmk: "**strong**", expect: expectMap{ encoderHTML: "<p><strong>strong</strong></p>", encoderMD: "__strong__", encoderSz: `(BLOCK (PARA (FORMAT-STRONG () (TEXT "strong"))))`, encoderSHTML: `((p (strong "strong")))`, encoderText: "strong", encoderZmk: useZmk, }, }, { descr: "Insert formatting", zmk: ">>insert>>", expect: expectMap{ encoderHTML: "<p><ins>insert</ins></p>", encoderMD: "insert", encoderSz: `(BLOCK (PARA (FORMAT-INSERT () (TEXT "insert"))))`, encoderSHTML: `((p (ins "insert")))`, encoderText: "insert", encoderZmk: useZmk, }, }, { descr: "Delete formatting", zmk: "~~delete~~", expect: expectMap{ encoderHTML: "<p><del>delete</del></p>", encoderMD: "delete", encoderSz: `(BLOCK (PARA (FORMAT-DELETE () (TEXT "delete"))))`, encoderSHTML: `((p (del "delete")))`, encoderText: "delete", encoderZmk: useZmk, }, }, { descr: "Update formatting", zmk: "~~old~~>>new>>", expect: expectMap{ encoderHTML: "<p><del>old</del><ins>new</ins></p>", encoderMD: "oldnew", encoderSz: `(BLOCK (PARA (FORMAT-DELETE () (TEXT "old")) (FORMAT-INSERT () (TEXT "new"))))`, encoderSHTML: `((p (del "old") (ins "new")))`, encoderText: "oldnew", encoderZmk: useZmk, }, }, { descr: "Superscript formatting", zmk: "^^superscript^^", expect: expectMap{ encoderHTML: `<p><sup>superscript</sup></p>`, encoderMD: "superscript", encoderSz: `(BLOCK (PARA (FORMAT-SUPER () (TEXT "superscript"))))`, encoderSHTML: `((p (sup "superscript")))`, encoderText: `superscript`, encoderZmk: useZmk, }, }, { descr: "Subscript formatting", zmk: ",,subscript,,", expect: expectMap{ encoderHTML: `<p><sub>subscript</sub></p>`, encoderMD: "subscript", encoderSz: `(BLOCK (PARA (FORMAT-SUB () (TEXT "subscript"))))`, encoderSHTML: `((p (sub "subscript")))`, encoderText: `subscript`, encoderZmk: useZmk, }, }, { descr: "Quotes formatting", zmk: `""quotes""`, expect: expectMap{ encoderHTML: "<p>“quotes”</p>", encoderMD: "“quotes”", encoderSz: `(BLOCK (PARA (FORMAT-QUOTE () (TEXT "quotes"))))`, encoderSHTML: `((p (@L (@H "“") "quotes" (@H "”"))))`, encoderText: `quotes`, encoderZmk: useZmk, }, }, { descr: "Quotes formatting (german)", zmk: `""quotes""{lang=de}`, expect: expectMap{ encoderHTML: `<p><span lang="de">„quotes“</span></p>`, encoderMD: "„quotes“", encoderSz: `(BLOCK (PARA (FORMAT-QUOTE (("lang" . "de")) (TEXT "quotes"))))`, encoderSHTML: `((p (span (@ (lang . "de")) (@H "„") "quotes" (@H "“"))))`, encoderText: `quotes`, encoderZmk: `""quotes""{lang="de"}`, }, }, { descr: "Empty quotes (default)", zmk: `""""`, expect: expectMap{ encoderHTML: `<p>“”</p>`, encoderMD: "“”", encoderSz: `(BLOCK (PARA (FORMAT-QUOTE ())))`, encoderSHTML: `((p (@L (@H "“" "”"))))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Empty quotes (unknown)", zmk: `""""{lang=unknown}`, expect: expectMap{ encoderHTML: `<p><span lang="unknown">""</span></p>`, encoderMD: """", encoderSz: `(BLOCK (PARA (FORMAT-QUOTE (("lang" . "unknown")))))`, encoderSHTML: `((p (span (@ (lang . "unknown")) (@H """ """))))`, encoderText: ``, encoderZmk: `""""{lang="unknown"}`, }, }, { descr: "Nested quotes (default)", zmk: `""say: ::""yes, ::""or?""::""::""`, expect: expectMap{ encoderHTML: `<p>“say: <span>‘yes, <span>“or?”</span>’</span>”</p>`, encoderMD: `“say: ‘yes, “or?”’”`, encoderSz: `(BLOCK (PARA (FORMAT-QUOTE () (TEXT "say: ") (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "yes, ") (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "or?"))))))))`, encoderSHTML: `((p (@L (@H "“") "say: " (span (@L (@H "‘") "yes, " (span (@L (@H "“") "or?" (@H "”"))) (@H "’"))) (@H "”"))))`, encoderText: `say: yes, or?`, encoderZmk: useZmk, }, }, { descr: "Two quotes", zmk: `""yes"" or ""no""`, expect: expectMap{ encoderHTML: `<p>“yes” or “no”</p>`, encoderMD: `“yes” or “no”`, encoderSz: `(BLOCK (PARA (FORMAT-QUOTE () (TEXT "yes")) (TEXT " or ") (FORMAT-QUOTE () (TEXT "no"))))`, encoderSHTML: `((p (@L (@H "“") "yes" (@H "”")) " or " (@L (@H "“") "no" (@H "”"))))`, encoderText: `yes or no`, encoderZmk: useZmk, }, }, { descr: "Mark formatting", zmk: `##marked##`, expect: expectMap{ encoderHTML: `<p><mark>marked</mark></p>`, encoderMD: "<mark>marked</mark>", encoderSz: `(BLOCK (PARA (FORMAT-MARK () (TEXT "marked"))))`, encoderSHTML: `((p (mark "marked")))`, encoderText: `marked`, encoderZmk: useZmk, }, }, { descr: "Span formatting", zmk: `::span::`, expect: expectMap{ encoderHTML: `<p><span>span</span></p>`, encoderMD: "span", encoderSz: `(BLOCK (PARA (FORMAT-SPAN () (TEXT "span"))))`, encoderSHTML: `((p (span "span")))`, encoderText: `span`, encoderZmk: useZmk, }, }, { descr: "Code formatting", zmk: "``code``", expect: expectMap{ encoderHTML: `<p><code>code</code></p>`, encoderMD: "`code`", encoderSz: `(BLOCK (PARA (LITERAL-CODE () "code")))`, encoderSHTML: `((p (code "code")))`, encoderText: `code`, encoderZmk: useZmk, }, }, { descr: "Code formatting with visible space", zmk: "``x y``{-}", expect: expectMap{ encoderHTML: "<p><code>x\u2423y</code></p>", encoderMD: "`x y`", encoderSz: `(BLOCK (PARA (LITERAL-CODE (("-" . "")) "x y")))`, encoderSHTML: "((p (code \"x\u2423y\")))", encoderText: `x y`, encoderZmk: useZmk, }, }, { descr: "HTML in code formatting", zmk: "``<script `` abc", expect: expectMap{ encoderHTML: "<p><code><script </code> abc</p>", encoderMD: "`<script ` abc", encoderSz: `(BLOCK (PARA (LITERAL-CODE () "<script ") (TEXT " abc")))`, encoderSHTML: `((p (code "<script ") " abc"))`, encoderText: `<script abc`, encoderZmk: useZmk, }, }, { descr: "Input formatting", zmk: `''input''`, expect: expectMap{ encoderHTML: `<p><kbd>input</kbd></p>`, encoderMD: "`input`", encoderSz: `(BLOCK (PARA (LITERAL-INPUT () "input")))`, encoderSHTML: `((p (kbd "input")))`, encoderText: `input`, encoderZmk: useZmk, }, }, { descr: "Output formatting", zmk: `==output==`, expect: expectMap{ encoderHTML: `<p><samp>output</samp></p>`, encoderMD: "`output`", encoderSz: `(BLOCK (PARA (LITERAL-OUTPUT () "output")))`, encoderSHTML: `((p (samp "output")))`, encoderText: `output`, encoderZmk: useZmk, }, }, { descr: "Math formatting", zmk: `$$\TeX$$`, expect: expectMap{ encoderHTML: `<p><code class="zs-math">\TeX</code></p>`, encoderMD: "\\TeX", encoderSz: `(BLOCK (PARA (LITERAL-MATH () "\\TeX")))`, encoderSHTML: `((p (code (@ (class . "zs-math")) "\\TeX")))`, encoderText: `\TeX`, encoderZmk: useZmk, }, }, { descr: "Nested Span Quote formatting", zmk: `::""abc""::{lang=fr}`, expect: expectMap{ encoderHTML: `<p><span lang="fr">« abc »</span></p>`, encoderMD: "« abc »", encoderSz: `(BLOCK (PARA (FORMAT-SPAN (("lang" . "fr")) (FORMAT-QUOTE () (TEXT "abc")))))`, encoderSHTML: `((p (span (@ (lang . "fr")) (@L (@H "«" " ") "abc" (@H " " "»")))))`, encoderText: `abc`, encoderZmk: `::""abc""::{lang="fr"}`, }, }, { descr: "Simple Citation", zmk: `[@Stern18]`, expect: expectMap{ encoderHTML: `<p><span>Stern18</span></p>`, // TODO encoderMD: "", encoderSz: `(BLOCK (PARA (CITE () "Stern18")))`, encoderSHTML: `((p (span "Stern18")))`, // TODO encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Citation", zmk: `[@Stern18 p.23]`, expect: expectMap{ encoderHTML: `<p><span>Stern18, p.23</span></p>`, // TODO encoderMD: "p.23", encoderSz: `(BLOCK (PARA (CITE () "Stern18" (TEXT "p.23"))))`, encoderSHTML: `((p (span "Stern18" ", " "p.23")))`, // TODO encoderText: `p.23`, encoderZmk: useZmk, }, }, { descr: "No comment", zmk: `% comment`, expect: expectMap{ encoderHTML: `<p>% comment</p>`, encoderMD: "% comment", encoderSz: `(BLOCK (PARA (TEXT "% comment")))`, encoderSHTML: `((p "% comment"))`, encoderText: `% comment`, encoderZmk: useZmk, }, }, { descr: "Line comment (nogen HTML)", zmk: `%% line comment`, expect: expectMap{ encoderHTML: `<p></p>`, encoderMD: "", encoderSz: `(BLOCK (PARA (LITERAL-COMMENT () "line comment")))`, encoderSHTML: `((p ()))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Line comment", zmk: `%%{-} line comment`, expect: expectMap{ encoderHTML: `<p><!-- line comment --></p>`, encoderMD: "", encoderSz: `(BLOCK (PARA (LITERAL-COMMENT (("-" . "")) "line comment")))`, encoderSHTML: `((p (@@ "line comment")))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Comment after text", zmk: `Text%%{-} comment`, expect: expectMap{ encoderHTML: `<p>Text<!-- comment --></p>`, encoderMD: "Text", encoderSz: `(BLOCK (PARA (TEXT "Text") (LITERAL-COMMENT (("-" . "")) "comment")))`, encoderSHTML: `((p "Text" (@@ "comment")))`, encoderText: `Text`, encoderZmk: useZmk, }, }, { descr: "Comment after text and with -->", zmk: `Text%%{-} comment --> end`, expect: expectMap{ encoderHTML: `<p>Text<!-- comment --> end --></p>`, encoderMD: "Text", encoderSz: `(BLOCK (PARA (TEXT "Text") (LITERAL-COMMENT (("-" . "")) "comment --> end")))`, encoderSHTML: `((p "Text" (@@ "comment --> end")))`, encoderText: `Text`, encoderZmk: useZmk, }, }, { descr: "Simple inline endnote", zmk: `[^endnote]`, expect: expectMap{ encoderHTML: `<p><sup id="fnref:1"><a class="zs-noteref" href="#fn:1" role="doc-noteref">1</a></sup></p><ol class="zs-endnotes"><li class="zs-endnote" id="fn:1" role="doc-endnote" value="1">endnote <a class="zs-endnote-backref" href="#fnref:1" role="doc-backlink">↩︎</a></li></ol>`, encoderMD: "", encoderSz: `(BLOCK (PARA (ENDNOTE () (TEXT "endnote"))))`, encoderSHTML: `((p (sup (@ (id . "fnref:1")) (a (@ (class . "zs-noteref") (href . "#fn:1") (role . "doc-noteref")) "1"))))`, encoderText: `endnote`, encoderZmk: useZmk, }, }, { descr: "Simple mark", zmk: `[!mark]`, expect: expectMap{ encoderHTML: `<p><a id="mark"></a></p>`, encoderMD: "", encoderSz: `(BLOCK (PARA (MARK "mark" "mark" "mark")))`, encoderSHTML: `((p (a (@ (id . "mark")))))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Mark with text", zmk: `[!mark|with text]`, expect: expectMap{ encoderHTML: `<p><a id="mark">with text</a></p>`, encoderMD: "with text", encoderSz: `(BLOCK (PARA (MARK "mark" "mark" "mark" (TEXT "with text"))))`, encoderSHTML: `((p (a (@ (id . "mark")) "with text")))`, encoderText: `with text`, encoderZmk: useZmk, }, }, { descr: "Invalid Link", zmk: `[[link|00000000000000]]`, expect: expectMap{ encoderHTML: `<p><span>link</span></p>`, encoderMD: "[link](00000000000000)", encoderSz: `(BLOCK (PARA (LINK () (INVALID "00000000000000") (TEXT "link"))))`, encoderSHTML: `((p (span "link")))`, encoderText: `link`, encoderZmk: useZmk, }, }, { descr: "Invalid Simple Link", zmk: `[[00000000000000]]`, expect: expectMap{ encoderHTML: `<p><span>00000000000000</span></p>`, encoderMD: "[00000000000000](00000000000000)", encoderSz: `(BLOCK (PARA (LINK () (INVALID "00000000000000"))))`, encoderSHTML: `((p (span "00000000000000")))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Dummy Link", zmk: `[[abc]]`, expect: expectMap{ encoderHTML: `<p><a href="abc">abc</a></p>`, encoderMD: "[abc](abc)", encoderSz: `(BLOCK (PARA (LINK () (HOSTED "abc"))))`, encoderSHTML: `((p (a (@ (href . "abc")) "abc")))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Simple URL", zmk: `[[https://zettelstore.de]]`, expect: expectMap{ encoderHTML: `<p><a href="https://zettelstore.de" rel="external">https://zettelstore.de</a></p>`, encoderMD: "<https://zettelstore.de>", encoderSz: `(BLOCK (PARA (LINK () (EXTERNAL "https://zettelstore.de"))))`, encoderSHTML: `((p (a (@ (href . "https://zettelstore.de") (rel . "external")) "https://zettelstore.de")))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "URL with Text", zmk: `[[Home|https://zettelstore.de]]`, expect: expectMap{ encoderHTML: `<p><a href="https://zettelstore.de" rel="external">Home</a></p>`, encoderMD: "[Home](https://zettelstore.de)", encoderSz: `(BLOCK (PARA (LINK () (EXTERNAL "https://zettelstore.de") (TEXT "Home"))))`, encoderSHTML: `((p (a (@ (href . "https://zettelstore.de") (rel . "external")) "Home")))`, encoderText: `Home`, encoderZmk: useZmk, }, }, { descr: "Simple Zettel ID", zmk: `[[00000000000100]]`, expect: expectMap{ encoderHTML: `<p><a href="00000000000100">00000000000100</a></p>`, encoderMD: "[00000000000100](00000000000100)", encoderSz: `(BLOCK (PARA (LINK () (ZETTEL "00000000000100"))))`, encoderSHTML: `((p (a (@ (href . "00000000000100")) "00000000000100")))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Zettel ID with Text", zmk: `[[Config|00000000000100]]`, expect: expectMap{ encoderHTML: `<p><a href="00000000000100">Config</a></p>`, encoderMD: "[Config](00000000000100)", encoderSz: `(BLOCK (PARA (LINK () (ZETTEL "00000000000100") (TEXT "Config"))))`, encoderSHTML: `((p (a (@ (href . "00000000000100")) "Config")))`, encoderText: `Config`, encoderZmk: useZmk, }, }, { descr: "Simple Zettel ID with fragment", zmk: `[[00000000000100#frag]]`, expect: expectMap{ encoderHTML: `<p><a href="00000000000100#frag">00000000000100#frag</a></p>`, encoderMD: "[00000000000100#frag](00000000000100#frag)", encoderSz: `(BLOCK (PARA (LINK () (ZETTEL "00000000000100#frag"))))`, encoderSHTML: `((p (a (@ (href . "00000000000100#frag")) "00000000000100#frag")))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Zettel ID with Text and fragment", zmk: `[[Config|00000000000100#frag]]`, expect: expectMap{ encoderHTML: `<p><a href="00000000000100#frag">Config</a></p>`, encoderMD: "[Config](00000000000100#frag)", encoderSz: `(BLOCK (PARA (LINK () (ZETTEL "00000000000100#frag") (TEXT "Config"))))`, encoderSHTML: `((p (a (@ (href . "00000000000100#frag")) "Config")))`, encoderText: `Config`, encoderZmk: useZmk, }, }, { descr: "Fragment link to self", zmk: `[[#frag]]`, expect: expectMap{ encoderHTML: `<p><a href="#frag">#frag</a></p>`, encoderMD: "[#frag](#frag)", encoderSz: `(BLOCK (PARA (LINK () (SELF "#frag"))))`, encoderSHTML: `((p (a (@ (href . "#frag")) "#frag")))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Hosted link", zmk: `[[H|/hosted]]`, expect: expectMap{ encoderHTML: `<p><a href="/hosted">H</a></p>`, encoderMD: "[H](/hosted)", encoderSz: `(BLOCK (PARA (LINK () (HOSTED "/hosted") (TEXT "H"))))`, encoderSHTML: `((p (a (@ (href . "/hosted")) "H")))`, encoderText: `H`, encoderZmk: useZmk, }, }, { descr: "Based link", zmk: `[[B|//based]]`, expect: expectMap{ encoderHTML: `<p><a href="/based">B</a></p>`, encoderMD: "[B](/based)", encoderSz: `(BLOCK (PARA (LINK () (BASED "/based") (TEXT "B"))))`, encoderText: `B`, encoderSHTML: `((p (a (@ (href . "/based")) "B")))`, encoderZmk: useZmk, }, }, { descr: "Relative link", zmk: `[[R|../relative]]`, expect: expectMap{ encoderHTML: `<p><a href="../relative">R</a></p>`, encoderMD: "[R](../relative)", encoderSz: `(BLOCK (PARA (LINK () (HOSTED "../relative") (TEXT "R"))))`, encoderSHTML: `((p (a (@ (href . "../relative")) "R")))`, encoderText: `R`, encoderZmk: useZmk, }, }, { descr: "Query link w/o text", zmk: `[[query:title:syntax]]`, expect: expectMap{ encoderHTML: `<p><a href="?q=title%3Asyntax">title:syntax</a></p>`, encoderMD: "", encoderSz: `(BLOCK (PARA (LINK () (QUERY "title:syntax"))))`, encoderSHTML: `((p (a (@ (href . "?q=title%3Asyntax")) "title:syntax")))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Query link with text", zmk: `[[Q|query:title:syntax]]`, expect: expectMap{ encoderHTML: `<p><a href="?q=title%3Asyntax">Q</a></p>`, encoderMD: "Q", encoderSz: `(BLOCK (PARA (LINK () (QUERY "title:syntax") (TEXT "Q"))))`, encoderSHTML: `((p (a (@ (href . "?q=title%3Asyntax")) "Q")))`, encoderText: `Q`, encoderZmk: useZmk, }, }, { descr: "Dummy Embed", zmk: `{{abc}}`, expect: expectMap{ encoderHTML: `<p><img src="abc"></p>`, encoderMD: "", encoderSz: `(BLOCK (PARA (EMBED () (HOSTED "abc") "")))`, encoderSHTML: `((p (img (@ (src . "abc")))))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "", zmk: ``, expect: expectMap{ encoderHTML: ``, encoderMD: "", encoderSz: `(BLOCK)`, encoderSHTML: `()`, encoderText: ``, encoderZmk: useZmk, }, }, } |
Added internal/encoder/encoder_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package encoder_test import ( "fmt" "strings" "testing" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/parser" ) type zmkTestCase struct { descr string zmk string inline bool expect expectMap } type expectMap map[api.EncodingEnum]string const useZmk = "\000" const ( encoderHTML = api.EncoderHTML encoderMD = api.EncoderMD encoderSz = api.EncoderSz encoderSHTML = api.EncoderSHTML encoderText = api.EncoderText encoderZmk = api.EncoderZmk ) func TestEncoder(t *testing.T) { for i := range tcsInline { tcsInline[i].inline = true } executeTestCases(t, append(tcsBlock, tcsInline...)) } func executeTestCases(t *testing.T, testCases []zmkTestCase) { for testNum, tc := range testCases { inp := input.NewInput([]byte(tc.zmk)) bs := parser.Parse(inp, nil, meta.ValueSyntaxZmk, config.NoHTML) checkEncodings(t, testNum, bs, tc.inline, tc.descr, tc.expect, tc.zmk) checkSz(t, testNum, bs, tc.inline, tc.descr) } } func checkEncodings(t *testing.T, testNum int, bs ast.BlockSlice, isInline bool, descr string, expected expectMap, zmkDefault string) { for enc, exp := range expected { encdr := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}) got, err := encode(encdr, bs) if err != nil { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } prefix += "\nMode: " + mode(isInline) t.Errorf("%s\nEncoder: %s\nError: %v", prefix, enc, err) continue } if enc == api.EncoderZmk && exp == useZmk { exp = zmkDefault } if got != exp { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } prefix += "\nMode: " + mode(isInline) t.Errorf("%s\nEncoder: %s\nExpected: %q\nGot: %q", prefix, enc, exp, got) } } } func checkSz(t *testing.T, testNum int, bs ast.BlockSlice, isInline bool, descr string) { t.Helper() encdr := encoder.Create(encoderSz, nil) exp, err := encode(encdr, bs) if err != nil { t.Error(err) return } val, err := sxreader.MakeReader(strings.NewReader(exp)).Read() if err != nil { t.Error(err) return } got := val.String() if exp != got { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } prefix += "\nMode: " + mode(isInline) t.Errorf("%s\n\nExpected: %q\nGot: %q", prefix, exp, got) } } func encode(e encoder.Encoder, bs ast.BlockSlice) (string, error) { var sb strings.Builder _, err := e.WriteBlocks(&sb, &bs) return sb.String(), err } func mode(isInline bool) string { if isInline { return "inline" } return "block" } |
Added internal/encoder/htmlenc.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package encoder // htmlenc encodes the abstract syntax tree into HTML5 via zettelstore-client. import ( "io" "strings" "t73f.de/r/sx" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" ) // htmlEncoder contains all data needed for encoding. type htmlEncoder struct { tx SzTransformer th *shtml.Evaluator lang string textEnc TextEncoder } // WriteZettel encodes a full zettel as HTML5. func (he *htmlEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { env := shtml.MakeEnvironment(he.lang) hm, err := he.th.Evaluate(he.tx.GetMeta(zn.InhMeta), &env) if err != nil { return 0, err } var isTitle ast.InlineSlice var htitle *sx.Pair plainTitle, hasTitle := zn.InhMeta.Get(meta.KeyTitle) if hasTitle { isTitle = ast.ParseSpacedText(string(plainTitle)) xtitle := he.tx.GetSz(&isTitle) htitle, err = he.th.Evaluate(xtitle, &env) if err != nil { return 0, err } } xast := he.tx.GetSz(&zn.BlocksAST) hast, err := he.th.Evaluate(xast, &env) if err != nil { return 0, err } hen := shtml.Endnotes(&env) var head sx.ListBuilder head.AddN( shtml.SymHead, sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.MakeString("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta), ) head.ExtendBang(hm) var sb strings.Builder if hasTitle { he.textEnc.WriteInlines(&sb, &isTitle) } else { sb.Write(zn.Meta.Zid.Bytes()) } head.Add(sx.MakeList(shtml.SymAttrTitle, sx.MakeString(sb.String()))) var body sx.ListBuilder body.Add(shtml.SymBody) if hasTitle { body.Add(htitle.Cons(shtml.SymH1)) } body.ExtendBang(hast) if hen != nil { body.AddN(sx.Cons(shtml.SymHR, nil), hen) } doc := sx.MakeList( sxhtml.SymDoctype, sx.MakeList(shtml.SymHTML, head.List(), body.List()), ) gen := sxhtml.NewGenerator().SetNewline() return gen.WriteHTML(w, doc) } // WriteMeta encodes meta data as HTML5. func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { env := shtml.MakeEnvironment(he.lang) hm, err := he.th.Evaluate(he.tx.GetMeta(m), &env) if err != nil { return 0, err } gen := sxhtml.NewGenerator().SetNewline() return gen.WriteListHTML(w, hm) } // WriteBlocks encodes a block slice. func (he *htmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { env := shtml.MakeEnvironment(he.lang) hobj, err := he.th.Evaluate(he.tx.GetSz(bs), &env) if err == nil { gen := sxhtml.NewGenerator() length, err2 := gen.WriteListHTML(w, hobj) if err2 != nil { return length, err2 } l, err2 := gen.WriteHTML(w, shtml.Endnotes(&env)) length += l return length, err2 } return 0, err } |
Added internal/encoder/mdenc.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package encoder // Encodes the abstract syntax tree back into Markdown. import ( "io" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "t73f.de/r/zsx" "zettelstore.de/z/internal/ast" ) // mdEncoder contains all data needed for encoding. type mdEncoder struct { lang string } // WriteZettel writes the encoded zettel to the writer. func (me *mdEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { v := newMDVisitor(w, me.lang) v.acceptMeta(zn.InhMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { v.b.WriteByte('\n') } ast.Walk(&v, &zn.BlocksAST) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as markdown. func (me *mdEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newMDVisitor(w, me.lang) v.acceptMeta(m) length, err := v.b.Flush() return length, err } func (v *mdVisitor) acceptMeta(m *meta.Meta) { for key, val := range m.Computed() { v.b.WriteStrings(key, ": ", string(val), "\n") } } // WriteBlocks writes the content of a block slice to the writer. func (me *mdEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { v := newMDVisitor(w, me.lang) ast.Walk(&v, bs) length, err := v.b.Flush() return length, err } // mdVisitor writes the abstract syntax tree to an EncWriter. type mdVisitor struct { b encWriter listInfo []int listPrefix string langStack shtml.LangStack quoteNesting uint } func newMDVisitor(w io.Writer, lang string) mdVisitor { return mdVisitor{b: newEncWriter(w), langStack: shtml.NewLangStack(lang)} } // pushAttribute adds the current attributes to the visitor. func (v *mdVisitor) pushAttributes(a zsx.Attributes) { if value, ok := a.Get("lang"); ok { v.langStack.Push(value) } else { v.langStack.Dup() } } // popAttributes removes the current attributes from the visitor. func (v *mdVisitor) popAttributes() { v.langStack.Pop() } // getLanguage returns the current language, func (v *mdVisitor) getLanguage() string { return v.langStack.Top() } func (v *mdVisitor) getQuotes() (string, string, bool) { qi := shtml.GetQuoteInfo(v.getLanguage()) leftQ, rightQ := qi.GetQuotes(v.quoteNesting) return leftQ, rightQ, qi.GetNBSp() } func (v *mdVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: v.visitBlockSlice(n) case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.visitHeading(n) case *ast.HRuleNode: v.b.WriteString("---") case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: return nil // Should write no content case *ast.TableNode: return nil // Should write no content case *ast.TextNode: v.b.WriteString(n.Text) case *ast.BreakNode: v.visitBreak(n) case *ast.LinkNode: v.visitLink(n) case *ast.EmbedRefNode: v.visitEmbedRef(n) case *ast.FootnoteNode: return nil // Should write no content case *ast.FormatNode: v.visitFormat(n) case *ast.LiteralNode: v.visitLiteral(n) default: return v } return nil } func (v *mdVisitor) visitBlockSlice(bs *ast.BlockSlice) { for i, bn := range *bs { if i > 0 { v.b.WriteString("\n\n") } ast.Walk(v, bn) } } func (v *mdVisitor) visitVerbatim(vn *ast.VerbatimNode) { lc := len(vn.Content) if vn.Kind != ast.VerbatimCode || lc == 0 { return } v.writeSpaces(4) lcm1 := lc - 1 for i := 0; i < lc; i++ { b := vn.Content[i] if b != '\n' && b != '\r' { v.b.WriteByte(b) continue } j := i + 1 for ; j < lc; j++ { c := vn.Content[j] if c != '\n' && c != '\r' { break } } if j >= lcm1 { break } v.b.WriteByte('\n') v.writeSpaces(4) i = j - 1 } } func (v *mdVisitor) visitRegion(rn *ast.RegionNode) { if rn.Kind != ast.RegionQuote { return } v.pushAttributes(rn.Attrs) defer v.popAttributes() first := true for _, bn := range rn.Blocks { pn, ok := bn.(*ast.ParaNode) if !ok { continue } if !first { v.b.WriteString("\n>\n") } first = false v.b.WriteString("> ") ast.Walk(v, &pn.Inlines) } } func (v *mdVisitor) visitHeading(hn *ast.HeadingNode) { v.pushAttributes(hn.Attrs) defer v.popAttributes() const headingSigns = "###### " v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-1:]) ast.Walk(v, &hn.Inlines) } func (v *mdVisitor) visitNestedList(ln *ast.NestedListNode) { switch ln.Kind { case ast.NestedListOrdered: v.writeNestedList(ln, "1. ") case ast.NestedListUnordered: v.writeNestedList(ln, "* ") case ast.NestedListQuote: v.writeListQuote(ln) } v.listInfo = v.listInfo[:len(v.listInfo)-1] } func (v *mdVisitor) writeNestedList(ln *ast.NestedListNode, enum string) { v.listInfo = append(v.listInfo, len(enum)) regIndent := 4*len(v.listInfo) - 4 paraIndent := regIndent + len(enum) for i, item := range ln.Items { if i > 0 { v.b.WriteByte('\n') } v.writeSpaces(regIndent) v.b.WriteString(enum) for j, in := range item { if j > 0 { v.b.WriteByte('\n') if _, ok := in.(*ast.ParaNode); ok { v.writeSpaces(paraIndent) } } ast.Walk(v, in) } } } func (v *mdVisitor) writeListQuote(ln *ast.NestedListNode) { v.listInfo = append(v.listInfo, 0) if len(v.listInfo) > 1 { return } prefix := v.listPrefix v.listPrefix = "> " for i, item := range ln.Items { if i > 0 { v.b.WriteByte('\n') } v.b.WriteString(v.listPrefix) for j, in := range item { if j > 0 { v.b.WriteByte('\n') if _, ok := in.(*ast.ParaNode); ok { v.b.WriteString(v.listPrefix) } } ast.Walk(v, in) } } v.listPrefix = prefix } func (v *mdVisitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("\\\n") } else { v.b.WriteByte('\n') } if l := len(v.listInfo); l > 0 { if v.listPrefix == "" { v.writeSpaces(4*l - 4 + v.listInfo[l-1]) } else { v.writeSpaces(4*l - 4) v.b.WriteString(v.listPrefix) } } } func (v *mdVisitor) visitLink(ln *ast.LinkNode) { v.pushAttributes(ln.Attrs) defer v.popAttributes() v.writeReference(ln.Ref, ln.Inlines) } func (v *mdVisitor) visitEmbedRef(en *ast.EmbedRefNode) { v.pushAttributes(en.Attrs) defer v.popAttributes() v.b.WriteByte('!') v.writeReference(en.Ref, en.Inlines) } func (v *mdVisitor) writeReference(ref *ast.Reference, is ast.InlineSlice) { if ref.State == ast.RefStateQuery { ast.Walk(v, &is) } else if len(is) > 0 { v.b.WriteByte('[') ast.Walk(v, &is) v.b.WriteStrings("](", ref.String()) v.b.WriteByte(')') } else if isAutoLinkable(ref) { v.b.WriteByte('<') v.b.WriteString(ref.String()) v.b.WriteByte('>') } else { s := ref.String() v.b.WriteStrings("[", s, "](", s, ")") } } func isAutoLinkable(ref *ast.Reference) bool { if ref.State != ast.RefStateExternal || ref.URL == nil { return false } return ref.URL.Scheme != "" } func (v *mdVisitor) visitFormat(fn *ast.FormatNode) { v.pushAttributes(fn.Attrs) defer v.popAttributes() switch fn.Kind { case ast.FormatEmph: v.b.WriteByte('*') ast.Walk(v, &fn.Inlines) v.b.WriteByte('*') case ast.FormatStrong: v.b.WriteString("__") ast.Walk(v, &fn.Inlines) v.b.WriteString("__") case ast.FormatQuote: v.writeQuote(fn) case ast.FormatMark: v.b.WriteString("<mark>") ast.Walk(v, &fn.Inlines) v.b.WriteString("</mark>") default: ast.Walk(v, &fn.Inlines) } } func (v *mdVisitor) writeQuote(fn *ast.FormatNode) { leftQ, rightQ, withNbsp := v.getQuotes() v.b.WriteString(leftQ) if withNbsp { v.b.WriteString(" ") } v.quoteNesting++ ast.Walk(v, &fn.Inlines) v.quoteNesting-- if withNbsp { v.b.WriteString(" ") } v.b.WriteString(rightQ) } func (v *mdVisitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralCode, ast.LiteralInput, ast.LiteralOutput: v.b.WriteByte('`') v.b.Write(ln.Content) v.b.WriteByte('`') case ast.LiteralComment: // ignore everything default: v.b.Write(ln.Content) } } func (v *mdVisitor) writeSpaces(n int) { for range n { v.b.WriteByte(' ') } } |
Added internal/encoder/shtmlenc.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | //----------------------------------------------------------------------------- // Copyright (c) 2023-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package encoder // shtmlenc encodes the abstract syntax tree into a s-expr which represents HTML. import ( "io" "t73f.de/r/sx" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" ) // shtmlEncoder contains all data needed for encoding. type shtmlEncoder struct { tx SzTransformer th *shtml.Evaluator lang string } // WriteZettel writes the encoded zettel to the writer. func (enc *shtmlEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { env := shtml.MakeEnvironment(enc.lang) metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(zn.InhMeta), &env) if err != nil { return 0, err } contentSHTML, err := enc.th.Evaluate(enc.tx.GetSz(&zn.BlocksAST), &env) if err != nil { return 0, err } result := sx.Cons(metaSHTML, contentSHTML) return result.Print(w) } // WriteMeta encodes meta data as s-expression. func (enc *shtmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { env := shtml.MakeEnvironment(enc.lang) metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(m), &env) if err != nil { return 0, err } return sx.Print(w, metaSHTML) } // WriteBlocks writes a block slice to the writer func (enc *shtmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { env := shtml.MakeEnvironment(enc.lang) hval, err := enc.th.Evaluate(enc.tx.GetSz(bs), &env) if err != nil { return 0, err } return sx.Print(w, hval) } |
Added internal/encoder/szenc.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package encoder // szenc encodes the abstract syntax tree into a s-expr for zettel. import ( "io" "t73f.de/r/sx" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" ) // szEncoder contains all data needed for encoding. type szEncoder struct { trans SzTransformer } // WriteZettel writes the encoded zettel to the writer. func (enc *szEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { content := enc.trans.GetSz(&zn.BlocksAST) meta := enc.trans.GetMeta(zn.InhMeta) return sx.MakeList(meta, content).Print(w) } // WriteMeta encodes meta data as s-expression. func (enc *szEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { return enc.trans.GetMeta(m).Print(w) } // WriteBlocks writes a block slice to the writer func (enc *szEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { return enc.trans.GetSz(bs).Print(w) } |
Added internal/encoder/sztransform.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package encoder import ( "encoding/base64" "fmt" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sz" "t73f.de/r/zsx" "zettelstore.de/z/internal/ast" ) // NewSzTransformer returns a new transformer to create s-expressions from AST nodes. func NewSzTransformer() SzTransformer { return SzTransformer{} } // SzTransformer contains all data needed to transform into a s-expression. type SzTransformer struct { inVerse bool } // GetSz transforms the given node into a sx list. func (t *SzTransformer) GetSz(node ast.Node) *sx.Pair { switch n := node.(type) { case *ast.BlockSlice: return zsx.MakeBlockList(t.getBlockList(n)) case *ast.InlineSlice: return zsx.MakeInlineList(t.getInlineList(*n)) case *ast.ParaNode: return zsx.MakeParaList(t.getInlineList(n.Inlines)) case *ast.VerbatimNode: return zsx.MakeVerbatim(mapGetS(mapVerbatimKindS, n.Kind), getAttributes(n.Attrs), string(n.Content)) case *ast.RegionNode: return t.getRegion(n) case *ast.HeadingNode: return zsx.MakeHeading(n.Level, getAttributes(n.Attrs), t.getInlineList(n.Inlines), n.Slug, n.Fragment) case *ast.HRuleNode: return zsx.MakeThematic(getAttributes(n.Attrs)) case *ast.NestedListNode: return t.getNestedList(n) case *ast.DescriptionListNode: return t.getDescriptionList(n) case *ast.TableNode: return t.getTable(n) case *ast.TranscludeNode: return zsx.MakeTransclusion(getAttributes(n.Attrs), getReference(n.Ref), t.getInlineList(n.Inlines)) case *ast.BLOBNode: return t.getBLOB(n) case *ast.TextNode: return zsx.MakeText(n.Text) case *ast.BreakNode: if n.Hard { return zsx.MakeHard() } return zsx.MakeSoft() case *ast.LinkNode: return t.getLink(n) case *ast.EmbedRefNode: return zsx.MakeEmbed(getAttributes(n.Attrs), getReference(n.Ref), n.Syntax, t.getInlineList(n.Inlines)) case *ast.EmbedBLOBNode: return t.getEmbedBLOB(n) case *ast.CiteNode: return zsx.MakeCite(getAttributes(n.Attrs), n.Key, t.getInlineList(n.Inlines)) case *ast.FootnoteNode: return zsx.MakeEndnote(getAttributes(n.Attrs), t.getInlineList(n.Inlines)) case *ast.MarkNode: return zsx.MakeMark(n.Mark, n.Slug, n.Fragment, t.getInlineList(n.Inlines)) case *ast.FormatNode: return zsx.MakeFormat(mapGetS(mapFormatKindS, n.Kind), getAttributes(n.Attrs), t.getInlineList(n.Inlines)) case *ast.LiteralNode: return zsx.MakeLiteral(mapGetS(mapLiteralKindS, n.Kind), getAttributes(n.Attrs), string(n.Content)) } return sx.MakeList(zsx.SymUnknown, sx.MakeString(fmt.Sprintf("%T %v", node, node))) } var mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{ ast.VerbatimZettel: zsx.SymVerbatimZettel, ast.VerbatimCode: zsx.SymVerbatimCode, ast.VerbatimEval: zsx.SymVerbatimEval, ast.VerbatimMath: zsx.SymVerbatimMath, ast.VerbatimComment: zsx.SymVerbatimComment, ast.VerbatimHTML: zsx.SymVerbatimHTML, } var mapFormatKindS = map[ast.FormatKind]*sx.Symbol{ ast.FormatEmph: zsx.SymFormatEmph, ast.FormatStrong: zsx.SymFormatStrong, ast.FormatDelete: zsx.SymFormatDelete, ast.FormatInsert: zsx.SymFormatInsert, ast.FormatSuper: zsx.SymFormatSuper, ast.FormatSub: zsx.SymFormatSub, ast.FormatQuote: zsx.SymFormatQuote, ast.FormatMark: zsx.SymFormatMark, ast.FormatSpan: zsx.SymFormatSpan, } var mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{ ast.LiteralCode: zsx.SymLiteralCode, ast.LiteralInput: zsx.SymLiteralInput, ast.LiteralOutput: zsx.SymLiteralOutput, ast.LiteralComment: zsx.SymLiteralComment, ast.LiteralMath: zsx.SymLiteralMath, } var mapRegionKindS = map[ast.RegionKind]*sx.Symbol{ ast.RegionSpan: zsx.SymRegionBlock, ast.RegionQuote: zsx.SymRegionQuote, ast.RegionVerse: zsx.SymRegionVerse, } func (t *SzTransformer) getRegion(rn *ast.RegionNode) *sx.Pair { saveInVerse := t.inVerse if rn.Kind == ast.RegionVerse { t.inVerse = true } symBlocks := t.getBlockList(&rn.Blocks) t.inVerse = saveInVerse return zsx.MakeRegion( mapGetS(mapRegionKindS, rn.Kind), getAttributes(rn.Attrs), symBlocks, t.getInlineList(rn.Inlines), ) } var mapNestedListKindS = map[ast.NestedListKind]*sx.Symbol{ ast.NestedListOrdered: zsx.SymListOrdered, ast.NestedListUnordered: zsx.SymListUnordered, ast.NestedListQuote: zsx.SymListQuote, } func (t *SzTransformer) getNestedList(ln *ast.NestedListNode) *sx.Pair { var items sx.ListBuilder isCompact := isCompactList(ln.Items) for _, item := range ln.Items { if isCompact && len(item) > 0 { paragraph := t.GetSz(item[0]) items.Add(zsx.MakeInlineList(paragraph.Tail())) continue } var itemObjs sx.ListBuilder for _, in := range item { itemObjs.Add(t.GetSz(in)) } if isCompact { items.Add(zsx.MakeInlineList(itemObjs.List())) } else { items.Add(zsx.MakeBlockList(itemObjs.List())) } } return zsx.MakeList(mapGetS(mapNestedListKindS, ln.Kind), getAttributes(ln.Attrs), items.List()) } func isCompactList(itemSlice []ast.ItemSlice) bool { for _, items := range itemSlice { if len(items) > 1 { return false } if len(items) == 1 { if _, ok := items[0].(*ast.ParaNode); !ok { return false } } } return true } func (t *SzTransformer) getDescriptionList(dn *ast.DescriptionListNode) *sx.Pair { var dlObjs sx.ListBuilder for _, def := range dn.Descriptions { dlObjs.Add(t.getInlineList(def.Term)) var descObjs sx.ListBuilder for _, b := range def.Descriptions { var dVal sx.ListBuilder for _, dn := range b { dVal.Add(t.GetSz(dn)) } descObjs.Add(zsx.MakeBlockList(dVal.List())) } dlObjs.Add(zsx.MakeBlockList(descObjs.List())) } return dlObjs.List().Cons(getAttributes(dn.Attrs)).Cons(zsx.SymDescription) } func (t *SzTransformer) getTable(tn *ast.TableNode) *sx.Pair { var lb sx.ListBuilder lb.AddN(zsx.SymTable, t.getHeader(tn.Header)) for _, row := range tn.Rows { lb.Add(t.getRow(row)) } return lb.List() } func (t *SzTransformer) getHeader(header ast.TableRow) *sx.Pair { if len(header) == 0 { return nil } return t.getRow(header) } func (t *SzTransformer) getRow(row ast.TableRow) *sx.Pair { var lb sx.ListBuilder for _, cell := range row { lb.Add(t.getCell(cell)) } return lb.List() } func (t *SzTransformer) getCell(cell *ast.TableCell) *sx.Pair { var attrs *sx.Pair switch cell.Align { case ast.AlignCenter: attrs = sx.Cons(sx.Cons(zsx.SymAttrAlign, zsx.AttrAlignCenter), nil) case ast.AlignLeft: attrs = sx.Cons(sx.Cons(zsx.SymAttrAlign, zsx.AttrAlignLeft), nil) case ast.AlignRight: attrs = sx.Cons(sx.Cons(zsx.SymAttrAlign, zsx.AttrAlignRight), nil) } return zsx.MakeCell(attrs, t.getInlineList(cell.Inlines)) } func (t *SzTransformer) getBLOB(bn *ast.BLOBNode) *sx.Pair { var content string if bn.Syntax == meta.ValueSyntaxSVG { content = string(bn.Blob) } else { content = getBase64String(bn.Blob) } return zsx.MakeBLOB(getAttributes(bn.Attrs), t.getInlineList(bn.Description), bn.Syntax, content) } func (t *SzTransformer) getLink(ln *ast.LinkNode) *sx.Pair { return zsx.MakeLink( getAttributes(ln.Attrs), getReference(ln.Ref), t.getInlineList(ln.Inlines), ) } func (t *SzTransformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair { var content string if en.Syntax == meta.ValueSyntaxSVG { content = string(en.Blob) } else { content = getBase64String(en.Blob) } return zsx.MakeEmbedBLOB(getAttributes(en.Attrs), en.Syntax, content, t.getInlineList(en.Inlines)) } func (t *SzTransformer) getBlockList(bs *ast.BlockSlice) *sx.Pair { var lb sx.ListBuilder for _, n := range *bs { lb.Add(t.GetSz(n)) } return lb.List() } func (t *SzTransformer) getInlineList(is ast.InlineSlice) *sx.Pair { var lb sx.ListBuilder for _, n := range is { lb.Add(t.GetSz(n)) } return lb.List() } func getAttributes(a zsx.Attributes) *sx.Pair { if a.IsEmpty() { return sx.Nil() } keys := a.Keys() var lb sx.ListBuilder for _, k := range keys { lb.Add(sx.Cons(sx.MakeString(k), sx.MakeString(a[k]))) } return lb.List() } var mapRefStateS = map[ast.RefState]*sx.Symbol{ ast.RefStateInvalid: zsx.SymRefStateInvalid, ast.RefStateZettel: sz.SymRefStateZettel, ast.RefStateSelf: zsx.SymRefStateSelf, ast.RefStateFound: sz.SymRefStateFound, ast.RefStateBroken: sz.SymRefStateBroken, ast.RefStateHosted: zsx.SymRefStateHosted, ast.RefStateBased: sz.SymRefStateBased, ast.RefStateQuery: sz.SymRefStateQuery, ast.RefStateExternal: zsx.SymRefStateExternal, } func getReference(ref *ast.Reference) *sx.Pair { return sx.MakeList(mapGetS(mapRefStateS, ref.State), sx.MakeString(ref.Value)) } var mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{ meta.TypeCredential: sz.SymTypeCredential, meta.TypeEmpty: sz.SymTypeEmpty, meta.TypeID: sz.SymTypeID, meta.TypeIDSet: sz.SymTypeIDSet, meta.TypeNumber: sz.SymTypeNumber, meta.TypeString: sz.SymTypeString, meta.TypeTagSet: sz.SymTypeTagSet, meta.TypeTimestamp: sz.SymTypeTimestamp, meta.TypeURL: sz.SymTypeURL, meta.TypeWord: sz.SymTypeWord, } // GetMeta transforms the given metadata into a sx list. func (t *SzTransformer) GetMeta(m *meta.Meta) *sx.Pair { var lb sx.ListBuilder lb.Add(sz.SymMeta) for key, val := range m.Computed() { ty := m.Type(key) symType := mapGetS(mapMetaTypeS, ty) var obj sx.Object if ty.IsSet { var setObjs sx.ListBuilder for _, val := range val.AsSlice() { setObjs.Add(sx.MakeString(val)) } obj = setObjs.List() } else { obj = sx.MakeString(string(val)) } lb.Add(sx.Nil().Cons(obj).Cons(sx.MakeSymbol(key)).Cons(symType)) } return lb.List() } func mapGetS[T comparable](m map[T]*sx.Symbol, k T) *sx.Symbol { if result, found := m[k]; found { return result } return sx.MakeSymbol(fmt.Sprintf("**%v:NOT-FOUND**", k)) } func getBase64String(data []byte) string { var sb strings.Builder encoder := base64.NewEncoder(base64.StdEncoding, &sb) _, err := encoder.Write(data) if err == nil { err = encoder.Close() } if err == nil { return sb.String() } return "" } |
Added internal/encoder/textenc.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package encoder // textenc encodes the abstract syntax tree into its text. import ( "io" "iter" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/ast" ) // TextEncoder encodes just the text and ignores any formatting. type TextEncoder struct{} // WriteZettel writes metadata and content. func (te *TextEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { v := newTextVisitor(w) te.WriteMeta(&v.b, zn.InhMeta) v.visitBlockSlice(&zn.BlocksAST) length, err := v.b.Flush() return length, err } // WriteMeta encodes metadata as text. func (te *TextEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { buf := newEncWriter(w) for key, val := range m.Computed() { if meta.Type(key) == meta.TypeTagSet { writeTagSet(&buf, val.Elems()) } else { buf.WriteString(string(val)) } buf.WriteByte('\n') } length, err := buf.Flush() return length, err } func writeTagSet(buf *encWriter, tags iter.Seq[meta.Value]) { first := true for tag := range tags { if !first { buf.WriteByte(' ') } first = false buf.WriteString(string(tag.CleanTag())) } } // WriteBlocks writes the content of a block slice to the writer. func (*TextEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { v := newTextVisitor(w) v.visitBlockSlice(bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (*TextEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { v := newTextVisitor(w) ast.Walk(&v, is) length, err := v.b.Flush() return length, err } // textVisitor writes the abstract syntax tree to an io.Writer. type textVisitor struct { b encWriter inlinePos int } func newTextVisitor(w io.Writer) textVisitor { return textVisitor{b: newEncWriter(w)} } func (v *textVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: v.visitBlockSlice(n) return nil case *ast.InlineSlice: v.visitInlineSlice(n) return nil case *ast.VerbatimNode: v.visitVerbatim(n) return nil case *ast.RegionNode: v.visitBlockSlice(&n.Blocks) if len(n.Inlines) > 0 { v.b.WriteByte('\n') ast.Walk(v, &n.Inlines) } return nil case *ast.NestedListNode: v.visitNestedList(n) return nil case *ast.DescriptionListNode: v.visitDescriptionList(n) return nil case *ast.TableNode: v.visitTable(n) return nil case *ast.TranscludeNode: ast.Walk(v, &n.Inlines) case *ast.BLOBNode: return nil case *ast.TextNode: v.visitText(n.Text) return nil case *ast.BreakNode: if n.Hard { v.b.WriteByte('\n') } else { v.b.WriteByte(' ') } return nil case *ast.LinkNode: if len(n.Inlines) > 0 { ast.Walk(v, &n.Inlines) } return nil case *ast.MarkNode: if len(n.Inlines) > 0 { ast.Walk(v, &n.Inlines) } return nil case *ast.FootnoteNode: if v.inlinePos > 0 { v.b.WriteByte(' ') } // No 'return nil' to write text case *ast.LiteralNode: if n.Kind != ast.LiteralComment { v.b.Write(n.Content) } } return v } func (v *textVisitor) visitVerbatim(vn *ast.VerbatimNode) { if vn.Kind == ast.VerbatimComment { return } v.b.Write(vn.Content) } func (v *textVisitor) visitNestedList(ln *ast.NestedListNode) { for i, item := range ln.Items { v.writePosChar(i, '\n') for j, it := range item { v.writePosChar(j, '\n') ast.Walk(v, it) } } } func (v *textVisitor) visitDescriptionList(dl *ast.DescriptionListNode) { for i, descr := range dl.Descriptions { v.writePosChar(i, '\n') ast.Walk(v, &descr.Term) for _, b := range descr.Descriptions { v.b.WriteByte('\n') for k, d := range b { v.writePosChar(k, '\n') ast.Walk(v, d) } } } } func (v *textVisitor) visitTable(tn *ast.TableNode) { if len(tn.Header) > 0 { v.writeRow(tn.Header) v.b.WriteByte('\n') } for i, row := range tn.Rows { v.writePosChar(i, '\n') v.writeRow(row) } } func (v *textVisitor) writeRow(row ast.TableRow) { for i, cell := range row { v.writePosChar(i, ' ') ast.Walk(v, &cell.Inlines) } } func (v *textVisitor) visitBlockSlice(bs *ast.BlockSlice) { for i, bn := range *bs { v.writePosChar(i, '\n') ast.Walk(v, bn) } } func (v *textVisitor) visitInlineSlice(is *ast.InlineSlice) { for i, in := range *is { v.inlinePos = i ast.Walk(v, in) } v.inlinePos = 0 } func (v *textVisitor) visitText(s string) { spaceFound := false for _, ch := range s { if input.IsSpace(ch) { if !spaceFound { v.b.WriteByte(' ') spaceFound = true } continue } spaceFound = false v.b.WriteString(string(ch)) } } func (v *textVisitor) writePosChar(pos int, ch byte) { if pos > 0 { v.b.WriteByte(ch) } } |
Added internal/encoder/write.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package encoder import ( "encoding/base64" "io" ) // encWriter is a specialized writer for encoding zettel. type encWriter struct { w io.Writer // The io.Writer to write to err error // Collect error length int // Collected length } // newEncWriter creates a new encWriter func newEncWriter(w io.Writer) encWriter { return encWriter{w: w} } // Write writes the content of p. func (w *encWriter) Write(p []byte) (l int, err error) { if w.err != nil { return 0, w.err } l, w.err = w.w.Write(p) w.length += l return l, w.err } // WriteString writes the content of s. func (w *encWriter) WriteString(s string) { if w.err != nil { return } var l int l, w.err = io.WriteString(w.w, s) w.length += l } // WriteStrings writes the content of sl. func (w *encWriter) WriteStrings(sl ...string) { for _, s := range sl { w.WriteString(s) } } // WriteByte writes the content of b. func (w *encWriter) WriteByte(b byte) error { var l int l, w.err = w.Write([]byte{b}) w.length += l return w.err } // WriteBytes writes the content of bs. func (w *encWriter) WriteBytes(bs ...byte) { w.Write(bs) } // WriteBase64 writes the content of p, encoded with base64. func (w *encWriter) WriteBase64(p []byte) { if w.err == nil { encoder := base64.NewEncoder(base64.StdEncoding, w.w) var l int l, w.err = encoder.Write(p) w.length += l err1 := encoder.Close() if w.err == nil { w.err = err1 } } } // Flush returns the collected length and error. func (w *encWriter) Flush() (int, error) { return w.length, w.err } |
Added internal/encoder/zmkenc.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package encoder // zmkenc encodes the abstract syntax tree back into Zettelmarkup. import ( "fmt" "io" "strings" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx" "zettelstore.de/z/internal/ast" ) // zmkEncoder contains all data needed for encoding. type zmkEncoder struct{} // WriteZettel writes the encoded zettel to the writer. func (*zmkEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { v := newZmkVisitor(w) v.acceptMeta(zn.InhMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { v.b.WriteByte('\n') } ast.Walk(&v, &zn.BlocksAST) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as zmk. func (*zmkEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newZmkVisitor(w) v.acceptMeta(m) length, err := v.b.Flush() return length, err } func (v *zmkVisitor) acceptMeta(m *meta.Meta) { for key, val := range m.Computed() { v.b.WriteStrings(key, ": ", string(val), "\n") } } // WriteBlocks writes the content of a block slice to the writer. func (*zmkEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { v := newZmkVisitor(w) ast.Walk(&v, bs) length, err := v.b.Flush() return length, err } // zmkVisitor writes the abstract syntax tree to an io.Writer. type zmkVisitor struct { b encWriter textEnc TextEncoder prefix []byte inVerse bool inlinePos int } func newZmkVisitor(w io.Writer) zmkVisitor { return zmkVisitor{b: newEncWriter(w)} } func (v *zmkVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: v.visitBlockSlice(n) case *ast.InlineSlice: for i, in := range *n { v.inlinePos = i ast.Walk(v, in) } v.inlinePos = 0 case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.visitHeading(n) case *ast.HRuleNode: v.b.WriteString("---") v.visitAttributes(n.Attrs) case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.TranscludeNode: v.b.WriteStrings("{{{", n.Ref.String(), "}}}") // FIXME n.Inlines v.visitAttributes(n.Attrs) case *ast.BLOBNode: v.visitBLOB(n) case *ast.TextNode: v.visitText(n) case *ast.BreakNode: v.visitBreak(n) case *ast.LinkNode: v.visitLink(n) case *ast.EmbedRefNode: v.visitEmbedRef(n) case *ast.EmbedBLOBNode: v.visitEmbedBLOB(n) case *ast.CiteNode: v.visitCite(n) case *ast.FootnoteNode: v.b.WriteString("[^") ast.Walk(v, &n.Inlines) v.b.WriteByte(']') v.visitAttributes(n.Attrs) case *ast.MarkNode: v.visitMark(n) case *ast.FormatNode: v.visitFormat(n) case *ast.LiteralNode: v.visitLiteral(n) default: return v } return nil } func (v *zmkVisitor) visitBlockSlice(bs *ast.BlockSlice) { var lastWasParagraph bool for i, bn := range *bs { if i > 0 { v.b.WriteByte('\n') if lastWasParagraph && !v.inVerse { if _, ok := bn.(*ast.ParaNode); ok { v.b.WriteByte('\n') } } } ast.Walk(v, bn) _, lastWasParagraph = bn.(*ast.ParaNode) } } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: "@@@", ast.VerbatimComment: "%%%", ast.VerbatimHTML: "@@@", // Attribute is set to {="html"} ast.VerbatimCode: "```", ast.VerbatimEval: "~~~", ast.VerbatimMath: "$$$", } func (v *zmkVisitor) visitVerbatim(vn *ast.VerbatimNode) { kind, ok := mapVerbatimKind[vn.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim kind %d", vn.Kind)) } attrs := vn.Attrs if vn.Kind == ast.VerbatimHTML { attrs = syntaxToHTML(attrs) } // TODO: scan cn.Lines to find embedded kind[0]s at beginning v.b.WriteString(kind) v.visitAttributes(attrs) v.b.WriteByte('\n') v.b.Write(vn.Content) v.b.WriteByte('\n') v.b.WriteString(kind) } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: ":::", ast.RegionQuote: "<<<", ast.RegionVerse: "\"\"\"", } func (v *zmkVisitor) visitRegion(rn *ast.RegionNode) { // Scan rn.Blocks for embedded regions to adjust length of regionCode kind, ok := mapRegionKind[rn.Kind] if !ok { panic(fmt.Sprintf("Unknown region kind %d", rn.Kind)) } v.b.WriteString(kind) v.visitAttributes(rn.Attrs) v.b.WriteByte('\n') saveInVerse := v.inVerse v.inVerse = rn.Kind == ast.RegionVerse ast.Walk(v, &rn.Blocks) v.inVerse = saveInVerse v.b.WriteByte('\n') v.b.WriteString(kind) if len(rn.Inlines) > 0 { v.b.WriteByte(' ') ast.Walk(v, &rn.Inlines) } } func (v *zmkVisitor) visitHeading(hn *ast.HeadingNode) { const headingSigns = "========= " v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:]) ast.Walk(v, &hn.Inlines) v.visitAttributes(hn.Attrs) } var mapNestedListKind = map[ast.NestedListKind]byte{ ast.NestedListOrdered: '#', ast.NestedListUnordered: '*', ast.NestedListQuote: '>', } func (v *zmkVisitor) visitNestedList(ln *ast.NestedListNode) { v.prefix = append(v.prefix, mapNestedListKind[ln.Kind]) for i, item := range ln.Items { if i > 0 { v.b.WriteByte('\n') } v.b.Write(v.prefix) v.b.WriteByte(' ') for j, in := range item { if j > 0 { v.b.WriteByte('\n') if _, ok := in.(*ast.ParaNode); ok { v.writePrefixSpaces() } } ast.Walk(v, in) } } v.prefix = v.prefix[:len(v.prefix)-1] } func (v *zmkVisitor) writePrefixSpaces() { if prefixLen := len(v.prefix); prefixLen > 0 { for i := 0; i <= prefixLen; i++ { v.b.WriteByte(' ') } } } func (v *zmkVisitor) visitDescriptionList(dn *ast.DescriptionListNode) { for i, descr := range dn.Descriptions { if i > 0 { v.b.WriteByte('\n') } v.b.WriteString("; ") ast.Walk(v, &descr.Term) for _, b := range descr.Descriptions { v.b.WriteString("\n: ") for jj, dn := range b { if jj > 0 { v.b.WriteString("\n\n ") } ast.Walk(v, dn) } } } } var alignCode = map[ast.Alignment]string{ ast.AlignDefault: "", ast.AlignLeft: "<", ast.AlignCenter: ":", ast.AlignRight: ">", } func (v *zmkVisitor) visitTable(tn *ast.TableNode) { if header := tn.Header; len(header) > 0 { v.writeTableHeader(header, tn.Align) v.b.WriteByte('\n') } for i, row := range tn.Rows { if i > 0 { v.b.WriteByte('\n') } v.writeTableRow(row, tn.Align) } } func (v *zmkVisitor) writeTableHeader(header ast.TableRow, align []ast.Alignment) { for pos, cell := range header { v.b.WriteString("|=") colAlign := align[pos] if cell.Align != colAlign { v.b.WriteString(alignCode[cell.Align]) } ast.Walk(v, &cell.Inlines) if colAlign != ast.AlignDefault { v.b.WriteString(alignCode[colAlign]) } } } func (v *zmkVisitor) writeTableRow(row ast.TableRow, align []ast.Alignment) { for pos, cell := range row { v.b.WriteByte('|') if cell.Align != align[pos] { v.b.WriteString(alignCode[cell.Align]) } ast.Walk(v, &cell.Inlines) } } func (v *zmkVisitor) visitBLOB(bn *ast.BLOBNode) { if bn.Syntax == meta.ValueSyntaxSVG { v.b.WriteStrings("@@@", bn.Syntax, "\n") v.b.Write(bn.Blob) v.b.WriteString("\n@@@\n") return } var sb strings.Builder v.textEnc.WriteInlines(&sb, &bn.Description) v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.") } var escapeSeqs = set.New( "\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", "##", ) func (v *zmkVisitor) visitText(tn *ast.TextNode) { last := 0 for i := 0; i < len(tn.Text); i++ { if b := tn.Text[i]; b == '\\' { v.b.WriteString(tn.Text[last:i]) v.b.WriteBytes('\\', b) last = i + 1 continue } if i < len(tn.Text)-1 { s := tn.Text[i : i+2] if escapeSeqs.Contains(s) { v.b.WriteString(tn.Text[last:i]) for j := range len(s) { v.b.WriteBytes('\\', s[j]) } i++ last = i + 1 continue } } } v.b.WriteString(tn.Text[last:]) } func (v *zmkVisitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("\\\n") } else { v.b.WriteByte('\n') } v.writePrefixSpaces() } func (v *zmkVisitor) visitLink(ln *ast.LinkNode) { v.b.WriteString("[[") if len(ln.Inlines) > 0 { ast.Walk(v, &ln.Inlines) v.b.WriteByte('|') } if ln.Ref.State == ast.RefStateBased { v.b.WriteByte('/') } v.b.WriteStrings(ln.Ref.String(), "]]") } func (v *zmkVisitor) visitEmbedRef(en *ast.EmbedRefNode) { v.b.WriteString("{{") if len(en.Inlines) > 0 { ast.Walk(v, &en.Inlines) v.b.WriteByte('|') } v.b.WriteStrings(en.Ref.String(), "}}") } func (v *zmkVisitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) { if en.Syntax == meta.ValueSyntaxSVG { v.b.WriteString("@@") v.b.Write(en.Blob) v.b.WriteStrings("@@{=", en.Syntax, "}") return } v.b.WriteString("{{TODO: display inline BLOB}}") } func (v *zmkVisitor) visitCite(cn *ast.CiteNode) { v.b.WriteStrings("[@", cn.Key) if len(cn.Inlines) > 0 { v.b.WriteByte(' ') ast.Walk(v, &cn.Inlines) } v.b.WriteByte(']') v.visitAttributes(cn.Attrs) } func (v *zmkVisitor) visitMark(mn *ast.MarkNode) { v.b.WriteStrings("[!", mn.Mark) if len(mn.Inlines) > 0 { v.b.WriteByte('|') ast.Walk(v, &mn.Inlines) } v.b.WriteByte(']') } var mapFormatKind = map[ast.FormatKind][]byte{ ast.FormatEmph: []byte("__"), ast.FormatStrong: []byte("**"), ast.FormatInsert: []byte(">>"), ast.FormatDelete: []byte("~~"), ast.FormatSuper: []byte("^^"), ast.FormatSub: []byte(",,"), ast.FormatQuote: []byte(`""`), ast.FormatMark: []byte("##"), ast.FormatSpan: []byte("::"), } func (v *zmkVisitor) visitFormat(fn *ast.FormatNode) { kind, ok := mapFormatKind[fn.Kind] if !ok { panic(fmt.Sprintf("Unknown format kind %d", fn.Kind)) } v.b.Write(kind) ast.Walk(v, &fn.Inlines) v.b.Write(kind) v.visitAttributes(fn.Attrs) } func (v *zmkVisitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralCode: v.writeLiteral('`', ln.Attrs, ln.Content) case ast.LiteralMath: v.b.WriteStrings("$$", string(ln.Content), "$$") v.visitAttributes(ln.Attrs) case ast.LiteralInput: v.writeLiteral('\'', ln.Attrs, ln.Content) case ast.LiteralOutput: v.writeLiteral('=', ln.Attrs, ln.Content) case ast.LiteralComment: v.b.WriteString("%%") v.visitAttributes(ln.Attrs) v.b.WriteByte(' ') v.b.Write(ln.Content) default: panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind)) } } func (v *zmkVisitor) writeLiteral(code byte, a zsx.Attributes, content []byte) { v.b.WriteBytes(code, code) v.writeEscaped(string(content), code) v.b.WriteBytes(code, code) v.visitAttributes(a) } // visitAttributes write HTML attributes func (v *zmkVisitor) visitAttributes(a zsx.Attributes) { if a.IsEmpty() { return } v.b.WriteByte('{') for i, k := range a.Keys() { if i > 0 { v.b.WriteByte(' ') } if k == "-" { v.b.WriteByte('-') continue } v.b.WriteString(k) if vl := a[k]; len(vl) > 0 { v.b.WriteStrings("=\"", vl) v.b.WriteByte('"') } } v.b.WriteByte('}') } func (v *zmkVisitor) writeEscaped(s string, toEscape byte) { last := 0 for i := range len(s) { if b := s[i]; b == toEscape || b == '\\' { v.b.WriteString(s[last:i]) v.b.WriteBytes('\\', b) last = i + 1 } } v.b.WriteString(s[last:]) } func syntaxToHTML(a zsx.Attributes) zsx.Attributes { return a.Clone().Set("", meta.ValueSyntaxHTML).Remove(meta.KeySyntax) } |
Added internal/evaluator/evaluator.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package evaluator interprets and evaluates the AST. package evaluator import ( "bytes" "context" "errors" "fmt" "path" "slices" "strconv" "strings" "t73f.de/r/sx/sxbuiltins" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) // Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel. type Port interface { GetZettel(context.Context, id.Zid) (zettel.Zettel, error) QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // EvaluateZettel evaluates the given zettel in the given context, with the // given ports, and the given environment. func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) { switch zn.Syntax { case meta.ValueSyntaxNone: // AST is empty, evaluate to a description list of metadata. zn.BlocksAST = evaluateMetadata(zn.Meta) case meta.ValueSyntaxSxn: zn.BlocksAST = evaluateSxn(zn.BlocksAST) default: EvaluateBlock(ctx, port, rtConfig, &zn.BlocksAST) } } func evaluateSxn(bs ast.BlockSlice) ast.BlockSlice { // Check for structure made in parser/plain/plain.go:parseSxnBlocks if len(bs) == 1 { // If len(bs) > 1 --> an error was found during parsing if vn, isVerbatim := bs[0].(*ast.VerbatimNode); isVerbatim && vn.Kind == ast.VerbatimCode { if classAttr, hasClass := vn.Attrs.Get(""); hasClass && classAttr == meta.ValueSyntaxSxn { rd := sxreader.MakeReader(bytes.NewReader(vn.Content)) if objs, err := rd.ReadAll(); err == nil { result := make(ast.BlockSlice, len(objs)) for i, obj := range objs { var buf bytes.Buffer sxbuiltins.Print(&buf, obj) result[i] = &ast.VerbatimNode{ Kind: ast.VerbatimCode, Attrs: zsx.Attributes{"": classAttr}, Content: buf.Bytes(), } } return result } } } } return bs } // EvaluateBlock evaluates the given block list in the given context, with // the given ports, and the given environment. func EvaluateBlock(ctx context.Context, port Port, rtConfig config.Config, bns *ast.BlockSlice) { e := evaluator{ ctx: ctx, port: port, rtConfig: rtConfig, transcludeMax: rtConfig.GetMaxTransclusions(), transcludeCount: 0, costMap: map[id.Zid]transcludeCost{}, embedMap: map[string]ast.InlineSlice{}, marker: &ast.ZettelNode{}, } ast.Walk(&e, bns) parser.Clean(bns, true) } type evaluator struct { ctx context.Context port Port rtConfig config.Config transcludeMax int transcludeCount int costMap map[id.Zid]transcludeCost marker *ast.ZettelNode embedMap map[string]ast.InlineSlice } type transcludeCost struct { zn *ast.ZettelNode ec int } func (e *evaluator) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: e.visitBlockSlice(n) case *ast.InlineSlice: e.visitInlineSlice(n) default: return e } return nil } func (e *evaluator) visitBlockSlice(bs *ast.BlockSlice) { for i := 0; i < len(*bs); i++ { bn := (*bs)[i] ast.Walk(e, bn) switch n := bn.(type) { case *ast.VerbatimNode: i += transcludeNode(bs, i, e.evalVerbatimNode(n)) case *ast.TranscludeNode: i += transcludeNode(bs, i, e.evalTransclusionNode(n)) } } } func transcludeNode(bln *ast.BlockSlice, i int, bn ast.BlockNode) int { if ln, ok := bn.(*ast.BlockSlice); ok { *bln = replaceWithBlockNodes(*bln, i, *ln) return len(*ln) - 1 } if bn == nil { (*bln) = (*bln)[:i+copy((*bln)[i:], (*bln)[i+1:])] return -1 } (*bln)[i] = bn return 0 } func replaceWithBlockNodes(bns []ast.BlockNode, i int, replaceBns []ast.BlockNode) []ast.BlockNode { if len(replaceBns) == 1 { bns[i] = replaceBns[0] return bns } newIns := make([]ast.BlockNode, 0, len(bns)+len(replaceBns)-1) if i > 0 { newIns = append(newIns, bns[:i]...) } if len(replaceBns) > 0 { newIns = append(newIns, replaceBns...) } if i+1 < len(bns) { newIns = append(newIns, bns[i+1:]...) } return newIns } func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode { switch vn.Kind { case ast.VerbatimZettel: return e.evalVerbatimZettel(vn) case ast.VerbatimEval: if syntax, found := vn.Attrs.Get(""); found && syntax == meta.ValueSyntaxDraw { return parser.ParseDrawBlock(vn) } } return vn } func (e *evaluator) evalVerbatimZettel(vn *ast.VerbatimNode) ast.BlockNode { m := meta.New(id.Invalid) m.Set(meta.KeySyntax, getSyntax(vn.Attrs, meta.ValueSyntaxText)) zettel := zettel.Zettel{ Meta: m, Content: zettel.NewContent(vn.Content), } e.transcludeCount++ zn := e.evaluateEmbeddedZettel(zettel) return &zn.BlocksAST } func getSyntax(a zsx.Attributes, defSyntax meta.Value) meta.Value { if a != nil { if val, ok := a.Get(meta.KeySyntax); ok { return meta.Value(val) } if val, ok := a.Get(""); ok { return meta.Value(val) } } return defSyntax } func (e *evaluator) evalTransclusionNode(tn *ast.TranscludeNode) ast.BlockNode { ref := tn.Ref // To prevent e.embedCount from counting if errText := e.checkMaxTransclusions(ref); errText != nil { return makeBlockNode(errText) } switch ref.State { case ast.RefStateZettel: // Only zettel references will be evaluated. case ast.RefStateInvalid, ast.RefStateBroken: e.transcludeCount++ return makeBlockNode(createInlineErrorText(ref, "Invalid", "or", "broken", "transclusion", "reference")) case ast.RefStateSelf: e.transcludeCount++ return makeBlockNode(createInlineErrorText(ref, "Self", "transclusion", "reference")) case ast.RefStateFound, ast.RefStateExternal: return tn case ast.RefStateHosted, ast.RefStateBased: if n := createEmbeddedNodeLocal(ref); n != nil { n.Attrs = tn.Attrs return makeBlockNode(n) } return tn case ast.RefStateQuery: e.transcludeCount++ return e.evalQueryTransclusion(tn.Ref.Value) default: return makeBlockNode(createInlineErrorText(ref, "Illegal", "block", "state", strconv.Itoa(int(ref.State)))) } zid, err := id.Parse(ref.URL.Path) if err != nil { panic(err) } cost, ok := e.costMap[zid] zn := cost.zn if zn == e.marker { e.transcludeCount++ return makeBlockNode(createInlineErrorText(ref, "Recursive", "transclusion")) } if !ok { zettel, err1 := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) if err1 != nil { if errors.Is(err1, &box.ErrNotAllowed{}) { return nil } e.transcludeCount++ return makeBlockNode(createInlineErrorText(ref, "Unable", "to", "get", "zettel")) } setMetadataFromAttributes(zettel.Meta, tn.Attrs) ec := e.transcludeCount e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec} zn = e.evaluateEmbeddedZettel(zettel) e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec} e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first. } e.transcludeCount++ if ec := cost.ec; ec > 0 { e.transcludeCount += cost.ec } return &zn.BlocksAST } func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode { q := query.Parse(expr) ml, err := e.port.QueryMeta(e.ctx, q) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return nil } return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel")) } result, _ := QueryAction(e.ctx, q, ml) if result != nil { ast.Walk(e, result) } return result } func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode { if maxTrans := e.transcludeMax; e.transcludeCount > maxTrans { e.transcludeCount = maxTrans + 1 return createInlineErrorText(ref, "Too", "many", "transclusions", "(must", "be", "at", "most", strconv.Itoa(maxTrans)+",", "see", "runtime", "configuration", "key", "max-transclusions)") } return nil } func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) } func setMetadataFromAttributes(m *meta.Meta, a zsx.Attributes) { for aKey, aVal := range a { if meta.KeyIsValid(aKey) { m.Set(aKey, meta.Value(aVal)) } } } func (e *evaluator) visitInlineSlice(is *ast.InlineSlice) { for i := 0; i < len(*is); i++ { in := (*is)[i] ast.Walk(e, in) switch n := in.(type) { case *ast.LinkNode: (*is)[i] = e.evalLinkNode(n) case *ast.EmbedRefNode: i += embedNode(is, i, e.evalEmbedRefNode(n)) } } } func embedNode(is *ast.InlineSlice, i int, in ast.InlineNode) int { if ln, ok := in.(*ast.InlineSlice); ok { *is = replaceWithInlineNodes(*is, i, *ln) return len(*ln) - 1 } if in == nil { (*is) = (*is)[:i+copy((*is)[i:], (*is)[i+1:])] return -1 } (*is)[i] = in return 0 } func replaceWithInlineNodes(ins ast.InlineSlice, i int, replaceIns ast.InlineSlice) ast.InlineSlice { if len(replaceIns) == 1 { ins[i] = replaceIns[0] return ins } newIns := make(ast.InlineSlice, 0, len(ins)+len(replaceIns)-1) if i > 0 { newIns = append(newIns, ins[:i]...) } if len(replaceIns) > 0 { newIns = append(newIns, replaceIns...) } if i+1 < len(ins) { newIns = append(newIns, ins[i+1:]...) } return newIns } func (e *evaluator) evalLinkNode(ln *ast.LinkNode) ast.InlineNode { if len(ln.Inlines) == 0 { ln.Inlines = ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}} } ref := ln.Ref if ref == nil || ref.State != ast.RefStateZettel { return ln } zid := mustParseZid(ref) _, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) if errors.Is(err, &box.ErrNotAllowed{}) { return &ast.FormatNode{ Kind: ast.FormatSpan, Attrs: ln.Attrs, Inlines: getLinkInline(ln), } } else if err != nil { ln.Ref.State = ast.RefStateBroken return ln } ln.Ref.State = ast.RefStateZettel return ln } func getLinkInline(ln *ast.LinkNode) ast.InlineSlice { if ln.Inlines != nil { return ln.Inlines } return ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}} } func (e *evaluator) evalEmbedRefNode(en *ast.EmbedRefNode) ast.InlineNode { ref := en.Ref // To prevent e.embedCount from counting if errText := e.checkMaxTransclusions(ref); errText != nil { return errText } switch ref.State { case ast.RefStateZettel: // Only zettel references will be evaluated. case ast.RefStateInvalid, ast.RefStateBroken: e.transcludeCount++ return createInlineErrorImage(en) case ast.RefStateSelf: e.transcludeCount++ return createInlineErrorText(ref, "Self", "embed", "reference") case ast.RefStateFound, ast.RefStateExternal: return en case ast.RefStateHosted, ast.RefStateBased: if n := createEmbeddedNodeLocal(ref); n != nil { n.Attrs = en.Attrs n.Inlines = en.Inlines return n } return en default: return createInlineErrorText(ref, "Illegal", "inline", "state", strconv.Itoa(int(ref.State))) } zid := mustParseZid(ref) zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return nil } e.transcludeCount++ return createInlineErrorImage(en) } if syntax := string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)); parser.IsImageFormat(syntax) { e.updateImageRefNode(en, zettel.Meta, syntax) return en } else if !parser.IsASTParser(syntax) { // Not embeddable. e.transcludeCount++ return createInlineErrorText(ref, "Not", "embeddable (syntax="+syntax+")") } cost, ok := e.costMap[zid] zn := cost.zn if zn == e.marker { e.transcludeCount++ return createInlineErrorText(ref, "Recursive", "transclusion") } if !ok { ec := e.transcludeCount e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec} zn = e.evaluateEmbeddedZettel(zettel) e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec} e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first. } e.transcludeCount++ result, ok := e.embedMap[ref.Value] if !ok { // Search for text to be embedded. result = findInlineSlice(&zn.BlocksAST, ref.URL.Fragment) e.embedMap[ref.Value] = result } if len(result) == 0 { return &ast.LiteralNode{ Kind: ast.LiteralComment, Attrs: map[string]string{"-": ""}, Content: append([]byte("Nothing to transclude: "), ref.String()...), } } if ec := cost.ec; ec > 0 { e.transcludeCount += cost.ec } return &result } func mustParseZid(ref *ast.Reference) id.Zid { zid, err := id.Parse(ref.URL.Path) if err != nil { panic(fmt.Sprintf("%v: %q (state %v) -> %v", err, ref.URL.Path, ref.State, ref)) } return zid } func (e *evaluator) updateImageRefNode(en *ast.EmbedRefNode, m *meta.Meta, syntax string) { en.Syntax = syntax if len(en.Inlines) == 0 { is := parser.ParseDescriptionAST(m) if len(is) > 0 { ast.Walk(e, &is) if len(is) > 0 { en.Inlines = is } } } } func createInlineErrorImage(en *ast.EmbedRefNode) *ast.EmbedRefNode { errorZid := id.ZidEmoji en.Ref = ast.ParseReference(errorZid.String()) if len(en.Inlines) == 0 { en.Inlines = ast.InlineSlice{&ast.TextNode{Text: "Error placeholder"}} } return en } func createInlineErrorText(ref *ast.Reference, msgWords ...string) ast.InlineNode { text := strings.Join(msgWords, " ") if ref != nil { text += ": " + ref.String() + "." } ln := &ast.LiteralNode{ Kind: ast.LiteralInput, Content: []byte(text), } fn := &ast.FormatNode{ Kind: ast.FormatStrong, Inlines: ast.InlineSlice{ln}, } fn.Attrs = fn.Attrs.AddClass("error") return fn } func createEmbeddedNodeLocal(ref *ast.Reference) *ast.EmbedRefNode { ext := path.Ext(ref.Value) if ext != "" && ext[0] == '.' { ext = ext[1:] } pinfo := parser.Get(ext) if pinfo == nil || !pinfo.IsImageFormat { return nil } return &ast.EmbedRefNode{ Ref: ref, Syntax: ext, } } func (e *evaluator) evaluateEmbeddedZettel(zettel zettel.Zettel) *ast.ZettelNode { zn := parser.ParseZettel(e.ctx, zettel, string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)), e.rtConfig) ast.Walk(e, &zn.BlocksAST) return zn } func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice { if fragment == "" { return firstInlinesToEmbed(*bs) } fs := fragmentSearcher{fragment: fragment} ast.Walk(&fs, bs) return fs.result } func firstInlinesToEmbed(bs ast.BlockSlice) ast.InlineSlice { if ins := bs.FirstParagraphInlines(); ins != nil { return ins } if len(bs) == 0 { return nil } if bn, ok := bs[0].(*ast.BLOBNode); ok { return ast.InlineSlice{&ast.EmbedBLOBNode{ Blob: bn.Blob, Syntax: bn.Syntax, Inlines: bn.Description, }} } return nil } type fragmentSearcher struct { fragment string result ast.InlineSlice } func (fs *fragmentSearcher) Visit(node ast.Node) ast.Visitor { if len(fs.result) > 0 { return nil } switch n := node.(type) { case *ast.BlockSlice: fs.visitBlockSlice(n) case *ast.InlineSlice: fs.visitInlineSlice(n) default: return fs } return nil } func (fs *fragmentSearcher) visitBlockSlice(bs *ast.BlockSlice) { for i, bn := range *bs { if hn, ok := bn.(*ast.HeadingNode); ok && hn.Fragment == fs.fragment { fs.result = (*bs)[i+1:].FirstParagraphInlines() return } ast.Walk(fs, bn) } } func (fs *fragmentSearcher) visitInlineSlice(is *ast.InlineSlice) { for i, in := range *is { if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment { ris := skipBreakeNodes((*is)[i+1:]) if len(mn.Inlines) > 0 { fs.result = slices.Clone(mn.Inlines) fs.result = append(fs.result, &ast.TextNode{Text: " "}) fs.result = append(fs.result, ris...) } else { fs.result = ris } return } ast.Walk(fs, in) } } func skipBreakeNodes(ins ast.InlineSlice) ast.InlineSlice { for i, in := range ins { switch in.(type) { case *ast.BreakNode: default: return ins[i:] } } return nil } |
Added internal/evaluator/list.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package evaluator import ( "bytes" "context" "math" "slices" "strconv" "strings" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/query" ) // QueryAction transforms a list of metadata according to query actions into a AST nested list. func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta) (ast.BlockNode, int) { ap := actionPara{ ctx: ctx, q: q, ml: ml, kind: ast.NestedListUnordered, minVal: -1, maxVal: -1, } actions := q.Actions() if len(actions) == 0 { return ap.createBlockNodeMeta("") } acts := make([]string, 0, len(actions)) for _, act := range actions { if strings.HasPrefix(act, api.NumberedAction[0:1]) { ap.kind = ast.NestedListOrdered continue } if strings.HasPrefix(act, api.MinAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.minVal = num continue } } if strings.HasPrefix(act, api.MaxAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.maxVal = num continue } } if act == api.ReIndexAction { continue } acts = append(acts, act) } var firstUnknowAct string for _, act := range acts { switch act { case api.KeysAction: return ap.createBlockNodeMetaKeys() } key := strings.ToLower(act) switch meta.Type(key) { case meta.TypeWord: return ap.createBlockNodeWord(key) case meta.TypeTagSet: return ap.createBlockNodeTagSet(key) } if firstUnknowAct == "" { firstUnknowAct = act } } bn, numItems := ap.createBlockNodeMeta(strings.ToLower(firstUnknowAct)) if bn != nil && numItems == 0 && firstUnknowAct == strings.ToUpper(firstUnknowAct) { bn, numItems = ap.createBlockNodeMeta("") } return bn, numItems } type actionPara struct { ctx context.Context q *query.Query ml []*meta.Meta kind ast.NestedListKind minVal int maxVal int } func (ap *actionPara) createBlockNodeWord(key string) (ast.BlockNode, int) { var buf bytes.Buffer ccs, bufLen := ap.prepareCatAction(key, &buf) if len(ccs) == 0 { return nil, 0 } items := make([]ast.ItemSlice, 0, len(ccs)) ccs.SortByName() for _, cat := range ccs { buf.WriteString(string(cat.Name)) items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(buf.String()), Inlines: ast.InlineSlice{&ast.TextNode{Text: string(cat.Name)}}, })}) buf.Truncate(bufLen) } return &ast.NestedListNode{ Kind: ap.kind, Items: items, Attrs: nil, }, len(items) } func (ap *actionPara) createBlockNodeTagSet(key string) (ast.BlockNode, int) { var buf bytes.Buffer ccs, bufLen := ap.prepareCatAction(key, &buf) if len(ccs) == 0 { return nil, 0 } ccs.SortByCount() ccs = ap.limitTags(ccs) countMap := ap.calcFontSizes(ccs) para := make(ast.InlineSlice, 0, len(ccs)) ccs.SortByName() for i, cat := range ccs { if i > 0 { para = append(para, &ast.TextNode{Text: " "}) } buf.WriteString(string(cat.Name)) para = append(para, &ast.LinkNode{ Attrs: countMap[cat.Count], Ref: ast.ParseReference(buf.String()), Inlines: ast.InlineSlice{ &ast.TextNode{Text: string(cat.Name)}, }, }, &ast.FormatNode{ Kind: ast.FormatSuper, Attrs: nil, Inlines: ast.InlineSlice{&ast.TextNode{Text: strconv.Itoa(cat.Count)}}, }, ) buf.Truncate(bufLen) } return &ast.ParaNode{Inlines: para}, len(ccs) } func (ap *actionPara) limitTags(ccs meta.CountedCategories) meta.CountedCategories { if minVal, maxVal := ap.minVal, ap.maxVal; minVal > 0 || maxVal > 0 { if minVal < 0 { minVal = ccs[len(ccs)-1].Count } if maxVal < 0 { maxVal = ccs[0].Count } if ccs[len(ccs)-1].Count < minVal || maxVal < ccs[0].Count { temp := make(meta.CountedCategories, 0, len(ccs)) for _, cat := range ccs { if minVal <= cat.Count && cat.Count <= maxVal { temp = append(temp, cat) } } return temp } } return ccs } func (ap *actionPara) createBlockNodeMetaKeys() (ast.BlockNode, int) { arr := make(meta.Arrangement, 128) for _, m := range ap.ml { for k := range m.Map() { arr[k] = append(arr[k], m) } } if len(arr) == 0 { return nil, 0 } ccs := arr.Counted() ccs.SortByName() var buf bytes.Buffer bufLen := ap.prepareSimpleQuery(&buf) items := make([]ast.ItemSlice, 0, len(ccs)) for _, cat := range ccs { buf.WriteString(string(cat.Name)) buf.WriteString(api.ExistOperator) q1 := buf.String() buf.Truncate(bufLen) buf.WriteString(api.ActionSeparator) buf.WriteString(string(cat.Name)) q2 := buf.String() buf.Truncate(bufLen) items = append(items, ast.ItemSlice{ast.CreateParaNode( &ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(q1), Inlines: ast.InlineSlice{&ast.TextNode{Text: string(cat.Name)}}, }, &ast.TextNode{Text: " "}, &ast.TextNode{Text: "(" + strconv.Itoa(cat.Count) + ", "}, &ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(q2), Inlines: ast.InlineSlice{&ast.TextNode{Text: "values"}}, }, &ast.TextNode{Text: ")"}, )}) } return &ast.NestedListNode{ Kind: ap.kind, Items: items, Attrs: nil, }, len(items) } func (ap *actionPara) createBlockNodeMeta(key string) (ast.BlockNode, int) { if len(ap.ml) == 0 { return nil, 0 } items := make([]ast.ItemSlice, 0, len(ap.ml)) for _, m := range ap.ml { if key != "" { if _, found := m.Get(key); !found { continue } } items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(m.Zid.String()), Inlines: ast.ParseSpacedText(m.GetTitle()), })}) } return &ast.NestedListNode{ Kind: ap.kind, Items: items, Attrs: nil, }, len(items) } func (ap *actionPara) prepareCatAction(key string, buf *bytes.Buffer) (meta.CountedCategories, int) { if len(ap.ml) == 0 { return nil, 0 } ccs := meta.CreateArrangement(ap.ml, key).Counted() if len(ccs) == 0 { return nil, 0 } ap.prepareSimpleQuery(buf) buf.WriteString(key) buf.WriteString(api.SearchOperatorHas) bufLen := buf.Len() return ccs, bufLen } func (ap *actionPara) prepareSimpleQuery(buf *bytes.Buffer) int { sea := ap.q.Clone() sea.RemoveActions() buf.WriteString(ast.QueryPrefix) sea.Print(buf) if buf.Len() > len(ast.QueryPrefix) { buf.WriteByte(' ') } return buf.Len() } const fontSizes = 6 // Must be the number of CSS classes zs-font-size-* in base.css const fontSizes64 = float64(fontSizes) func (*actionPara) calcFontSizes(ccs meta.CountedCategories) map[int]zsx.Attributes { var fsAttrs [fontSizes]zsx.Attributes var a zsx.Attributes for i := range fontSizes { fsAttrs[i] = a.AddClass("zs-font-size-" + strconv.Itoa(i)) } countMap := make(map[int]int, len(ccs)) for _, cat := range ccs { countMap[cat.Count]++ } countList := make([]int, 0, len(countMap)) for count := range countMap { countList = append(countList, count) } slices.Sort(countList) result := make(map[int]zsx.Attributes, len(countList)) if len(countList) <= fontSizes { // If we have less different counts, center them inside the fsAttrs vector. curSize := (fontSizes - len(countList)) / 2 for _, count := range countList { result[count] = fsAttrs[curSize] curSize++ } return result } // Idea: the number of occurences for a specific count is substracted from a budget. total := float64(len(ccs)) curSize := 0 budget := calcBudget(total, 0.0) for _, count := range countList { result[count] = fsAttrs[curSize] cc := float64(countMap[count]) total -= cc budget -= cc if budget < 1 { curSize++ if curSize >= fontSizes { curSize = fontSizes budget = 0.0 } else { budget = calcBudget(total, float64(curSize)) } } } return result } func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) } |
Added internal/evaluator/metadata.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package evaluator import ( "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" ) func evaluateMetadata(m *meta.Meta) ast.BlockSlice { descrlist := &ast.DescriptionListNode{} for key, val := range m.All() { descrlist.Descriptions = append( descrlist.Descriptions, getMetadataDescription(key, val)) } return ast.BlockSlice{descrlist} } func getMetadataDescription(key string, value meta.Value) ast.Description { is := convertMetavalueToInlineSlice(value, meta.Type(key)) return ast.Description{ Term: ast.InlineSlice{&ast.TextNode{Text: key}}, Descriptions: []ast.DescriptionSlice{{&ast.ParaNode{Inlines: is}}}, } } func convertMetavalueToInlineSlice(value meta.Value, dt *meta.DescriptionType) ast.InlineSlice { var sliceData []string if dt.IsSet { sliceData = value.AsSlice() if len(sliceData) == 0 { return nil } } else { sliceData = []string{string(value)} } makeLink := dt == meta.TypeID || dt == meta.TypeIDSet result := make(ast.InlineSlice, 0, 2*len(sliceData)-1) for i, val := range sliceData { if i > 0 { result = append(result, &ast.TextNode{Text: " "}) } tn := &ast.TextNode{Text: val} if makeLink { result = append(result, &ast.LinkNode{ Ref: ast.ParseReference(val), Inlines: ast.InlineSlice{tn}, }) } else { result = append(result, tn) } } return result } |
Added internal/kernel/auth.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "errors" "sync" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/logger" ) type authService struct { srvConfig mxService sync.RWMutex manager auth.Manager createManager CreateAuthManagerFunc } var errAlreadySetOwner = errors.New("changing an existing owner not allowed") var errAlreadyROMode = errors.New("system in readonly mode cannot change this mode") func (as *authService) Initialize(logger *logger.Logger) { as.logger = logger as.descr = descriptionMap{ AuthOwner: { "Owner's zettel id", func(val string) (any, error) { if owner := as.cur[AuthOwner]; owner != nil && owner != id.Invalid { return nil, errAlreadySetOwner } if val == "" { return id.Invalid, nil } return parseZid(val) }, false, }, AuthReadonly: { "Readonly mode", func(val string) (any, error) { if ro := as.cur[AuthReadonly]; ro == true { return nil, errAlreadyROMode } return parseBool(val) }, true, }, } as.next = interfaceMap{ AuthOwner: id.Invalid, AuthReadonly: false, } } func (as *authService) GetLogger() *logger.Logger { return as.logger } func (as *authService) Start(*Kernel) error { as.mxService.Lock() defer as.mxService.Unlock() readonlyMode := as.GetNextConfig(AuthReadonly).(bool) owner := as.GetNextConfig(AuthOwner).(id.Zid) authMgr, err := as.createManager(readonlyMode, owner) if err != nil { as.logger.Error().Err(err).Msg("Unable to create manager") return err } as.logger.Info().Msg("Start Manager") as.manager = authMgr return nil } func (as *authService) IsStarted() bool { as.mxService.RLock() defer as.mxService.RUnlock() return as.manager != nil } func (as *authService) Stop(*Kernel) { as.logger.Info().Msg("Stop Manager") as.mxService.Lock() as.manager = nil as.mxService.Unlock() } func (*authService) GetStatistics() []KeyValue { return nil } |
Added internal/kernel/box.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "context" "errors" "fmt" "io" "net/url" "strconv" "sync" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/logger" ) type boxService struct { srvConfig mxService sync.RWMutex manager box.Manager createManager CreateBoxManagerFunc } var errInvalidDirType = errors.New("invalid directory type") func (ps *boxService) Initialize(logger *logger.Logger) { ps.logger = logger ps.descr = descriptionMap{ BoxDefaultDirType: { "Default directory box type", ps.noFrozen(func(val string) (any, error) { switch val { case BoxDirTypeNotify, BoxDirTypeSimple: return val, nil } return nil, errInvalidDirType }), true, }, BoxURIs: { "Box URI", func(val string) (any, error) { uVal, err := url.Parse(val) if err != nil { return nil, err } if uVal.Scheme == "" { uVal.Scheme = "dir" } return uVal, nil }, true, }, } ps.next = interfaceMap{ BoxDefaultDirType: BoxDirTypeNotify, } } func (ps *boxService) GetLogger() *logger.Logger { return ps.logger } func (ps *boxService) Start(kern *Kernel) error { boxURIs := make([]*url.URL, 0, 4) for i := 1; ; i++ { u := ps.GetNextConfig(BoxURIs + strconv.Itoa(i)) if u == nil { break } boxURIs = append(boxURIs, u.(*url.URL)) } ps.mxService.Lock() defer ps.mxService.Unlock() mgr, err := ps.createManager(boxURIs, kern.auth.manager, &kern.cfg) if err != nil { ps.logger.Error().Err(err).Msg("Unable to create manager") return err } ps.logger.Info().Str("location", mgr.Location()).Msg("Start Manager") if err = mgr.Start(context.Background()); err != nil { ps.logger.Error().Err(err).Msg("Unable to start manager") return err } kern.cfg.setBox(mgr) ps.manager = mgr return nil } func (ps *boxService) IsStarted() bool { ps.mxService.RLock() defer ps.mxService.RUnlock() return ps.manager != nil } func (ps *boxService) Stop(*Kernel) { ps.logger.Info().Msg("Stop Manager") ps.mxService.RLock() mgr := ps.manager ps.mxService.RUnlock() mgr.Stop(context.Background()) ps.mxService.Lock() ps.manager = nil ps.mxService.Unlock() } func (ps *boxService) GetStatistics() []KeyValue { var st box.Stats ps.mxService.RLock() ps.manager.ReadStats(&st) ps.mxService.RUnlock() return []KeyValue{ {Key: "Read-only", Value: strconv.FormatBool(st.ReadOnly)}, {Key: "Managed boxes", Value: strconv.Itoa(st.NumManagedBoxes)}, {Key: "Zettel (total)", Value: strconv.Itoa(st.ZettelTotal)}, {Key: "Zettel (indexed)", Value: strconv.Itoa(st.ZettelIndexed)}, {Key: "Last re-index", Value: st.LastReload.Format("2006-01-02 15:04:05 -0700 MST")}, {Key: "Duration last re-index", Value: fmt.Sprintf("%vms", st.DurLastReload.Milliseconds())}, {Key: "Indexes since last re-index", Value: strconv.FormatUint(st.IndexesSinceReload, 10)}, {Key: "Indexed words", Value: strconv.FormatUint(st.IndexedWords, 10)}, {Key: "Indexed URLs", Value: strconv.FormatUint(st.IndexedUrls, 10)}, {Key: "Zettel enrichments", Value: strconv.FormatUint(st.IndexUpdates, 10)}, } } func (ps *boxService) dumpIndex(w io.Writer) { ps.manager.Dump(w) } func (ps *boxService) Refresh() error { ps.mxService.RLock() defer ps.mxService.RUnlock() if ps.manager != nil { return ps.manager.Refresh(context.Background()) } return nil } |
Added internal/kernel/cfg.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "context" "errors" "fmt" "strconv" "strings" "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/web/server" ) type configService struct { srvConfig mxService sync.RWMutex orig *meta.Meta manager box.Manager } // Predefined Metadata keys for runtime configuration // See: https://zettelstore.de/manual/h/00001004020000 const ( keyDefaultCopyright = "default-copyright" keyDefaultLicense = "default-license" keyDefaultVisibility = "default-visibility" keyExpertMode = "expert-mode" keyMaxTransclusions = "max-transclusions" keySiteName = "site-name" keyYAMLHeader = "yaml-header" keyZettelFileSyntax = "zettel-file-syntax" ) var errUnknownVisibility = errors.New("unknown visibility") func (cs *configService) Initialize(logger *logger.Logger) { cs.logger = logger cs.descr = descriptionMap{ keyDefaultCopyright: {"Default copyright", parseString, true}, keyDefaultLicense: {"Default license", parseString, true}, keyDefaultVisibility: { "Default zettel visibility", func(val string) (any, error) { vis := meta.Value(val).AsVisibility() if vis == meta.VisibilityUnknown { return nil, errUnknownVisibility } return vis, nil }, true, }, keyExpertMode: {"Expert mode", parseBool, true}, config.KeyFooterZettel: {"Footer Zettel", parseInvalidZid, true}, config.KeyHomeZettel: {"Home zettel", parseZid, true}, ConfigInsecureHTML: { "Insecure HTML", cs.noFrozen(func(val string) (any, error) { switch val { case ConfigSyntaxHTML: return config.SyntaxHTML, nil case ConfigMarkdownHTML: return config.MarkdownHTML, nil case ConfigZmkHTML: return config.ZettelmarkupHTML, nil } return config.NoHTML, nil }), true, }, meta.KeyLang: {"Language", parseString, true}, keyMaxTransclusions: {"Maximum transclusions", parseInt64, true}, keySiteName: {"Site name", parseString, true}, keyYAMLHeader: {"YAML header", parseBool, true}, keyZettelFileSyntax: { "Zettel file syntax", func(val string) (any, error) { return strings.Fields(val), nil }, true, }, ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true}, config.KeyListsMenuZettel: {"Lists menu", parseZid, true}, config.KeyShowBackLinks: {"Show back links", parseString, true}, config.KeyShowFolgeLinks: {"Show folge links", parseString, true}, config.KeyShowSequelLinks: {"Show sequel links", parseString, true}, config.KeyShowSubordinateLinks: {"Show subordinate links", parseString, true}, config.KeyShowSuccessorLinks: {"Show successor links", parseString, true}, } cs.next = interfaceMap{ keyDefaultCopyright: "", keyDefaultLicense: "", keyDefaultVisibility: meta.VisibilityLogin, keyExpertMode: false, config.KeyFooterZettel: id.Invalid, config.KeyHomeZettel: id.ZidDefaultHome, ConfigInsecureHTML: config.NoHTML, meta.KeyLang: meta.ValueLangEN, keyMaxTransclusions: int64(1024), keySiteName: "Zettelstore", keyYAMLHeader: false, keyZettelFileSyntax: nil, ConfigSimpleMode: false, config.KeyListsMenuZettel: id.ZidTOCListsMenu, config.KeyShowBackLinks: "", config.KeyShowFolgeLinks: "", config.KeyShowSequelLinks: "", config.KeyShowSubordinateLinks: "", config.KeyShowSuccessorLinks: "", } } func (cs *configService) GetLogger() *logger.Logger { return cs.logger } func (cs *configService) Start(*Kernel) error { cs.logger.Info().Msg("Start Service") data := meta.New(id.ZidConfiguration) for _, kv := range cs.GetNextConfigList() { data.Set(kv.Key, meta.Value(kv.Value)) } cs.mxService.Lock() cs.orig = data cs.mxService.Unlock() return nil } func (cs *configService) IsStarted() bool { cs.mxService.RLock() defer cs.mxService.RUnlock() return cs.orig != nil } func (cs *configService) Stop(*Kernel) { cs.logger.Info().Msg("Stop Service") cs.mxService.Lock() cs.orig = nil cs.manager = nil cs.mxService.Unlock() } func (*configService) GetStatistics() []KeyValue { return nil } func (cs *configService) setBox(mgr box.Manager) { cs.mxService.Lock() cs.manager = mgr cs.mxService.Unlock() mgr.RegisterObserver(cs.observe) cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ZidConfiguration}) } func (cs *configService) doUpdate(p box.BaseBox) error { z, err := p.GetZettel(context.Background(), id.ZidConfiguration) cs.logger.Trace().Err(err).Msg("got config meta") if err != nil { return err } m := z.Meta cs.mxService.Lock() for key := range cs.orig.All() { if val, ok := m.Get(key); ok { cs.SetConfig(key, string(val)) } else if defVal, defFound := cs.orig.Get(key); defFound { cs.SetConfig(key, string(defVal)) } } cs.mxService.Unlock() cs.SwitchNextToCur() // Poor man's restart return nil } func (cs *configService) observe(ci box.UpdateInfo) { if (ci.Reason != box.OnZettel && ci.Reason != box.OnDelete) || ci.Zid == id.ZidConfiguration { cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe") go func() { cs.mxService.RLock() mgr := cs.manager cs.mxService.RUnlock() if mgr != nil { cs.doUpdate(mgr) } else { cs.doUpdate(ci.Box) } }() } } // --- config.Config func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string { if m != nil { if val, found := m.Get(key); found { return string(val) } } if user := server.GetUser(ctx); user != nil { if val, found := user.Get(key); found { return string(val) } } result := cs.GetCurConfig(key) if result == nil { return "" } switch val := result.(type) { case string: return val case bool: if val { return meta.ValueTrue } return meta.ValueFalse case id.Zid: return val.String() case int: return strconv.Itoa(val) case []string: return strings.Join(val, " ") case meta.Visibility: return val.String() case fmt.Stringer: return val.String() case fmt.GoStringer: return val.GoString() } return fmt.Sprintf("%v", result) } // AddDefaultValues enriches the given meta data with its default values. func (cs *configService) AddDefaultValues(ctx context.Context, m *meta.Meta) *meta.Meta { if cs == nil { return m } result := m cs.mxService.RLock() if _, found := m.Get(meta.KeyCopyright); !found { result = updateMeta(result, m, meta.KeyCopyright, cs.GetCurConfig(keyDefaultCopyright).(string)) } if _, found := m.Get(meta.KeyLang); !found { result = updateMeta(result, m, meta.KeyLang, cs.Get(ctx, nil, meta.KeyLang)) } if _, found := m.Get(meta.KeyLicense); !found { result = updateMeta(result, m, meta.KeyLicense, cs.GetCurConfig(keyDefaultLicense).(string)) } if _, found := m.Get(meta.KeyVisibility); !found { result = updateMeta(result, m, meta.KeyVisibility, cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility).String()) } cs.mxService.RUnlock() return result } func updateMeta(result, m *meta.Meta, key string, val string) *meta.Meta { if result == m { result = m.Clone() } result.Set(key, meta.Value(val)) return result } func (cs *configService) GetHTMLInsecurity() config.HTMLInsecurity { return cs.GetCurConfig(ConfigInsecureHTML).(config.HTMLInsecurity) } // GetSiteName returns the current value of the "site-name" key. func (cs *configService) GetSiteName() string { return cs.GetCurConfig(keySiteName).(string) } // GetMaxTransclusions return the maximum number of indirect transclusions. func (cs *configService) GetMaxTransclusions() int { return int(cs.GetCurConfig(keyMaxTransclusions).(int64)) } // GetYAMLHeader returns the current value of the "yaml-header" key. func (cs *configService) GetYAMLHeader() bool { return cs.GetCurConfig(keyYAMLHeader).(bool) } // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. func (cs *configService) GetZettelFileSyntax() []meta.Value { if zfs := cs.GetCurConfig(keyZettelFileSyntax); zfs != nil { zfsAS := zfs.([]string) result := make([]meta.Value, len(zfsAS)) for i, fs := range zfsAS { result[i] = meta.Value(fs) } return result } return nil } // --- config.AuthConfig // GetSimpleMode returns true if system tuns in simple-mode. func (cs *configService) GetSimpleMode() bool { return cs.GetCurConfig(ConfigSimpleMode).(bool) } // GetExpertMode returns the current value of the "expert-mode" key. func (cs *configService) GetExpertMode() bool { return cs.GetCurConfig(keyExpertMode).(bool) } // GetVisibility returns the visibility value, or "login" if none is given. func (cs *configService) GetVisibility(m *meta.Meta) meta.Visibility { if val, ok := m.Get(meta.KeyVisibility); ok { if vis := val.AsVisibility(); vis != meta.VisibilityUnknown { return vis } } vis := cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility) if vis != meta.VisibilityUnknown { return vis } cs.mxService.RLock() val, _ := cs.orig.Get(keyDefaultVisibility) vis = val.AsVisibility() cs.mxService.RUnlock() return vis } |
Added internal/kernel/cmd.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "fmt" "io" "maps" "os" "runtime/metrics" "slices" "strconv" "strings" "zettelstore.de/z/internal/logger" "zettelstore.de/z/strfun" ) type cmdSession struct { w io.Writer kern *Kernel echo bool header bool colwidth int eol []byte } func (sess *cmdSession) initialize(w io.Writer, kern *Kernel) { sess.w = w sess.kern = kern sess.header = true sess.colwidth = 80 sess.eol = []byte{'\n'} } func (sess *cmdSession) executeLine(line string) bool { if sess.echo { sess.println(line) } cmd, args := splitLine(line) if c, ok := commands[cmd]; ok { return c.Func(sess, cmd, args) } if cmd == "help" { return cmdHelp(sess, cmd, args) } sess.println("Unknown command:", cmd, strings.Join(args, " ")) sess.println("-- Enter 'help' go get a list of valid commands.") return true } func (sess *cmdSession) println(args ...string) { if len(args) > 0 { io.WriteString(sess.w, args[0]) for _, arg := range args[1:] { io.WriteString(sess.w, " ") io.WriteString(sess.w, arg) } } sess.w.Write(sess.eol) } func (sess *cmdSession) usage(cmd, val string) { sess.println("Usage:", cmd, val) } func (sess *cmdSession) printTable(table [][]string) { maxLen := sess.calcMaxLen(table) if len(maxLen) == 0 { return } if sess.header { sess.printRow(table[0], maxLen, "|=", " | ", ' ') hLine := make([]string, len(table[0])) sess.printRow(hLine, maxLen, "|%", "-+-", '-') } for _, row := range table[1:] { sess.printRow(row, maxLen, "| ", " | ", ' ') } } func (sess *cmdSession) calcMaxLen(table [][]string) []int { maxLen := make([]int, 0) for _, row := range table { for colno, column := range row { if colno >= len(maxLen) { maxLen = append(maxLen, 0) } colLen := strfun.Length(column) if colLen <= maxLen[colno] { continue } maxLen[colno] = min(colLen, sess.colwidth) } } return maxLen } func (sess *cmdSession) printRow(row []string, maxLen []int, prefix, delim string, pad rune) { for colno, column := range row { io.WriteString(sess.w, prefix) prefix = delim io.WriteString(sess.w, strfun.JustifyLeft(column, maxLen[colno], pad)) } sess.w.Write(sess.eol) } func splitLine(line string) (string, []string) { s := strings.Fields(line) if len(s) == 0 { return "", nil } return strings.ToLower(s[0]), s[1:] } type command struct { Text string Func func(sess *cmdSession, cmd string, args []string) bool } var commands = map[string]command{ "": {"", func(*cmdSession, string, []string) bool { return true }}, "bye": { "end this session", func(*cmdSession, string, []string) bool { return false }, }, "config": {"show configuration keys", cmdConfig}, "crlf": { "toggle crlf mode", func(sess *cmdSession, _ string, _ []string) bool { if len(sess.eol) == 1 { sess.eol = []byte{'\r', '\n'} sess.println("crlf is on") } else { sess.eol = []byte{'\n'} sess.println("crlf is off") } return true }, }, "dump-index": {"writes the content of the index", cmdDumpIndex}, "dump-recover": {"show data of last recovery", cmdDumpRecover}, "echo": { "toggle echo mode", func(sess *cmdSession, _ string, _ []string) bool { sess.echo = !sess.echo if sess.echo { sess.println("echo is on") } else { sess.println("echo is off") } return true }, }, "end-profile": {"stop profiling", cmdEndProfile}, "env": {"show environment values", cmdEnvironment}, "get-config": {"show current configuration data", cmdGetConfig}, "header": { "toggle table header", func(sess *cmdSession, _ string, _ []string) bool { sess.header = !sess.header if sess.header { sess.println("header are on") } else { sess.println("header are off") } return true }, }, "log-level": {"get/set log level", cmdLogLevel}, "metrics": {"show Go runtime metrics", cmdMetrics}, "next-config": {"show next configuration data", cmdNextConfig}, "profile": {"start profiling", cmdProfile}, "refresh": {"refresh box data", cmdRefresh}, "restart": {"restart service", cmdRestart}, "services": {"show available services", cmdServices}, "set-config": {"set next configuration data", cmdSetConfig}, "shutdown": { "shutdown Zettelstore", func(sess *cmdSession, _ string, _ []string) bool { sess.kern.Shutdown(false); return false }, }, "start": {"start service", cmdStart}, "stat": {"show service statistics", cmdStat}, "stop": {"stop service", cmdStop}, } func cmdHelp(sess *cmdSession, _ string, _ []string) bool { table := [][]string{{"Command", "Description"}} for _, cmd := range slices.Sorted(maps.Keys(commands)) { table = append(table, []string{cmd, commands[cmd].Text}) } sess.printTable(table) return true } func cmdConfig(sess *cmdSession, cmd string, args []string) bool { srvnum, ok := lookupService(sess, cmd, args) if !ok { return true } srv := sess.kern.srvs[srvnum].srv table := [][]string{{"Key", "Description"}} for _, kd := range srv.ConfigDescriptions() { table = append(table, []string{kd.Key, kd.Descr}) } sess.printTable(table) return true } func cmdGetConfig(sess *cmdSession, _ string, args []string) bool { showConfig(sess, args, listCurConfig, func(srv service, key string) any { return srv.GetCurConfig(key) }) return true } func cmdNextConfig(sess *cmdSession, _ string, args []string) bool { showConfig(sess, args, listNextConfig, func(srv service, key string) any { return srv.GetNextConfig(key) }) return true } func showConfig(sess *cmdSession, args []string, listConfig func(*cmdSession, service), getConfig func(service, string) any) { if len(args) == 0 { keys := make([]int, 0, len(sess.kern.srvs)) for k := range sess.kern.srvs { keys = append(keys, int(k)) } slices.Sort(keys) for i, k := range keys { if i > 0 { sess.println() } srvD := sess.kern.srvs[Service(k)] sess.println("%% Service", srvD.name) listConfig(sess, srvD.srv) } return } srvD, found := getService(sess, args[0]) if !found { return } if len(args) == 1 { listConfig(sess, srvD.srv) return } val := getConfig(srvD.srv, args[1]) if val == nil { sess.println("Unknown key", args[1], "for service", args[0]) return } sess.println(fmt.Sprintf("%v", val)) } func listCurConfig(sess *cmdSession, srv service) { listConfig(sess, func() []KeyDescrValue { return srv.GetCurConfigList(true) }) } func listNextConfig(sess *cmdSession, srv service) { listConfig(sess, srv.GetNextConfigList) } func listConfig(sess *cmdSession, getConfigList func() []KeyDescrValue) { l := getConfigList() table := [][]string{{"Key", "Value", "Description"}} for _, kdv := range l { table = append(table, []string{kdv.Key, kdv.Value, kdv.Descr}) } sess.printTable(table) } func cmdSetConfig(sess *cmdSession, cmd string, args []string) bool { if len(args) < 3 { sess.usage(cmd, "SERVICE KEY VALUE") return true } srvD, found := getService(sess, args[0]) if !found { return true } key := args[1] newValue := strings.Join(args[2:], " ") if err := srvD.srv.SetConfig(key, newValue); err == nil { sess.kern.logger.Mandatory().Str("key", key).Str("value", newValue).Msg("Update system configuration") } else { sess.println("Unable to set key", args[1], "to value", newValue, "because:", err.Error()) } return true } func cmdServices(sess *cmdSession, _ string, _ []string) bool { table := [][]string{{"Service", "Status"}} for _, name := range sortedServiceNames(sess) { if sess.kern.srvNames[name].srv.IsStarted() { table = append(table, []string{name, "started"}) } else { table = append(table, []string{name, "stopped"}) } } sess.printTable(table) return true } func cmdStart(sess *cmdSession, cmd string, args []string) bool { srvnum, ok := lookupService(sess, cmd, args) if !ok { return true } err := sess.kern.doStartService(srvnum) if err != nil { sess.println(err.Error()) } return true } func cmdRestart(sess *cmdSession, cmd string, args []string) bool { srvnum, ok := lookupService(sess, cmd, args) if !ok { return true } err := sess.kern.restartService(srvnum) if err != nil { sess.println(err.Error()) } return true } func cmdStop(sess *cmdSession, cmd string, args []string) bool { if srvnum, ok := lookupService(sess, cmd, args); ok { sess.kern.doStopService(srvnum) } return true } func cmdStat(sess *cmdSession, cmd string, args []string) bool { if len(args) == 0 { sess.usage(cmd, "SERVICE") return true } srvD, ok := getService(sess, args[0]) if !ok { return true } kvl := srvD.srv.GetStatistics() if len(kvl) == 0 { return true } table := [][]string{{"Key", "Value"}} for _, kv := range kvl { table = append(table, []string{kv.Key, kv.Value}) } sess.printTable(table) return true } func cmdLogLevel(sess *cmdSession, _ string, args []string) bool { kern := sess.kern if len(args) == 0 { // Write log levels level := kern.logger.Level() table := [][]string{ {"Service", "Level", "Name"}, {"kernel", strconv.Itoa(int(level)), level.String()}, } for _, name := range sortedServiceNames(sess) { level = kern.srvNames[name].srv.GetLogger().Level() table = append(table, []string{name, strconv.Itoa(int(level)), level.String()}) } sess.printTable(table) return true } var l *logger.Logger name := args[0] if name == "kernel" { l = kern.logger } else { srvD, ok := getService(sess, name) if !ok { return true } l = srvD.srv.GetLogger() } if len(args) == 1 { level := l.Level() sess.println(strconv.Itoa(int(level)), level.String()) return true } level := args[1] uval, err := strconv.ParseUint(level, 10, 8) lv := logger.Level(uval) if err != nil || !lv.IsValid() { lv = logger.ParseLevel(level) } if !lv.IsValid() { sess.println("Invalid level:", level) return true } kern.logger.Mandatory().Str("name", name).Str("level", lv.String()).Msg("Update log level") l.SetLevel(lv) return true } func lookupService(sess *cmdSession, cmd string, args []string) (Service, bool) { if len(args) == 0 { sess.usage(cmd, "SERVICE") return 0, false } srvD, ok := getService(sess, args[0]) if !ok { return 0, false } return srvD.srvnum, true } func cmdProfile(sess *cmdSession, _ string, args []string) bool { var profileName string if len(args) < 1 { profileName = ProfileCPU } else { profileName = args[0] } var fileName string if len(args) < 2 { fileName = profileName + ".prof" } else { fileName = args[1] } kern := sess.kern if err := kern.doStartProfiling(profileName, fileName); err != nil { sess.println("Error:", err.Error()) } else { kern.logger.Mandatory().Str("profile", profileName).Str("file", fileName).Msg("Start profiling") } return true } func cmdEndProfile(sess *cmdSession, _ string, _ []string) bool { kern := sess.kern err := kern.doStopProfiling() if err != nil { sess.println("Error:", err.Error()) } kern.logger.Mandatory().Err(err).Msg("Stop profiling") return true } func cmdMetrics(sess *cmdSession, _ string, _ []string) bool { var samples []metrics.Sample all := metrics.All() for _, d := range all { if d.Kind == metrics.KindFloat64Histogram { continue } samples = append(samples, metrics.Sample{Name: d.Name}) } metrics.Read(samples) table := [][]string{{"Value", "Description"}} i := 0 for _, d := range all { if d.Kind == metrics.KindFloat64Histogram { continue } descr := d.Description if pos := strings.IndexByte(descr, '.'); pos > 0 { descr = descr[:pos] } value := samples[i].Value i++ var sVal string switch value.Kind() { case metrics.KindUint64: sVal = strconv.FormatUint(value.Uint64(), 10) case metrics.KindFloat64: sVal = fmt.Sprintf("%v", value.Float64()) case metrics.KindFloat64Histogram: sVal = "(Histogramm)" case metrics.KindBad: sVal = "BAD" default: sVal = fmt.Sprintf("(unexpected metric kind: %v)", value.Kind()) } table = append(table, []string{sVal, descr}) } sess.printTable(table) return true } func cmdDumpIndex(sess *cmdSession, _ string, _ []string) bool { sess.kern.dumpIndex(sess.w) return true } func cmdRefresh(sess *cmdSession, _ string, _ []string) bool { kern := sess.kern kern.logger.Mandatory().Msg("Refresh") kern.box.Refresh() return true } func cmdDumpRecover(sess *cmdSession, cmd string, args []string) bool { if len(args) == 0 { sess.usage(cmd, "RECOVER") sess.println("-- A valid value for RECOVER can be obtained via 'stat core'.") return true } lines := sess.kern.core.RecoverLines(args[0]) if len(lines) == 0 { return true } for _, line := range lines { sess.println(line) } return true } func cmdEnvironment(sess *cmdSession, _ string, _ []string) bool { workDir, err := os.Getwd() if err != nil { workDir = err.Error() } execName, err := os.Executable() if err != nil { execName = err.Error() } envs := os.Environ() slices.Sort(envs) table := [][]string{ {"Key", "Value"}, {"workdir", workDir}, {"executable", execName}, } for _, env := range envs { if pos := strings.IndexByte(env, '='); pos >= 0 && pos < len(env) { table = append(table, []string{env[:pos], env[pos+1:]}) } } sess.printTable(table) return true } func sortedServiceNames(sess *cmdSession) []string { return slices.Sorted(maps.Keys(sess.kern.srvNames)) } func getService(sess *cmdSession, name string) (serviceData, bool) { srvD, found := sess.kern.srvNames[name] if !found { sess.println("Unknown service", name) } return srvD, found } |
Added internal/kernel/config.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "errors" "fmt" "maps" "slices" "strconv" "strings" "sync" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/logger" ) type parseFunc func(string) (any, error) type configDescription struct { text string parse parseFunc canList bool } type descriptionMap map[string]configDescription type interfaceMap map[string]any func (m interfaceMap) Clone() interfaceMap { return maps.Clone(m) } type srvConfig struct { logger *logger.Logger mxConfig sync.RWMutex frozen bool descr descriptionMap cur interfaceMap next interfaceMap } func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() keys := slices.Sorted(maps.Keys(cfg.descr)) result := make([]serviceConfigDescription, 0, len(keys)) for _, k := range keys { text := cfg.descr[k].text if strings.HasSuffix(k, "-") { text = text + " (list)" } result = append(result, serviceConfigDescription{Key: k, Descr: text}) } return result } var errAlreadyFrozen = errors.New("value not allowed to be set") func (cfg *srvConfig) noFrozen(parse parseFunc) parseFunc { return func(val string) (any, error) { if cfg.frozen { return nil, errAlreadyFrozen } return parse(val) } } var errListKeyNotFound = errors.New("no list key found") func (cfg *srvConfig) SetConfig(key, value string) error { cfg.mxConfig.Lock() defer cfg.mxConfig.Unlock() descr, ok := cfg.descr[key] if !ok { d, baseKey, num := cfg.getListDescription(key) if num < 0 { return errListKeyNotFound } for i := num + 1; ; i++ { k := baseKey + strconv.Itoa(i) if _, ok = cfg.next[k]; !ok { break } delete(cfg.next, k) } if num == 0 { return nil } descr = d } parse := descr.parse if parse == nil { if cfg.frozen { return errAlreadyFrozen } cfg.next[key] = value return nil } iVal, err := parse(value) if err != nil { return err } cfg.next[key] = iVal return nil } func (cfg *srvConfig) getListDescription(key string) (configDescription, string, int) { for k, d := range cfg.descr { if !strings.HasSuffix(k, "-") { continue } if !strings.HasPrefix(key, k) { continue } num, err := strconv.Atoi(key[len(k):]) if err != nil || num < 0 { continue } return d, k, num } return configDescription{}, "", -1 } func (cfg *srvConfig) GetCurConfig(key string) any { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() if cfg.cur == nil { return cfg.next[key] } return cfg.cur[key] } func (cfg *srvConfig) GetNextConfig(key string) any { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() return cfg.next[key] } func (cfg *srvConfig) GetCurConfigList(all bool) []KeyDescrValue { return cfg.getOneConfigList(all, cfg.GetCurConfig) } func (cfg *srvConfig) GetNextConfigList() []KeyDescrValue { return cfg.getOneConfigList(true, cfg.GetNextConfig) } func (cfg *srvConfig) getOneConfigList(all bool, getConfig func(string) any) []KeyDescrValue { if len(cfg.descr) == 0 { return nil } keys := cfg.getSortedConfigKeys(all, getConfig) result := make([]KeyDescrValue, 0, len(keys)) for _, k := range keys { val := getConfig(k) if val == nil { continue } descr, ok := cfg.descr[k] if !ok { descr, _, _ = cfg.getListDescription(k) } result = append(result, KeyDescrValue{ Key: k, Descr: descr.text, Value: fmt.Sprintf("%v", val), }) } return result } func (cfg *srvConfig) getSortedConfigKeys(all bool, getConfig func(string) any) []string { keys := make([]string, 0, len(cfg.descr)) for k, descr := range cfg.descr { if all || descr.canList { if !strings.HasSuffix(k, "-") { keys = append(keys, k) continue } for i := 1; ; i++ { key := k + strconv.Itoa(i) val := getConfig(key) if val == nil { break } keys = append(keys, key) } } } slices.Sort(keys) return keys } func (cfg *srvConfig) Freeze() { cfg.mxConfig.Lock() cfg.frozen = true cfg.mxConfig.Unlock() } func (cfg *srvConfig) SwitchNextToCur() { cfg.mxConfig.Lock() defer cfg.mxConfig.Unlock() cfg.cur = cfg.next.Clone() } func parseString(val string) (any, error) { return val, nil } var errNoBoolean = errors.New("no boolean value") func parseBool(val string) (any, error) { if val == "" { return false, errNoBoolean } switch val[0] { case '0', 'f', 'F', 'n', 'N': return false, nil } return true, nil } func parseInt64(val string) (any, error) { u64, err := strconv.ParseInt(val, 10, 64) if err == nil { return u64, nil } return nil, err } func parseZid(val string) (any, error) { zid, err := id.Parse(val) if err == nil { return zid, nil } return id.Invalid, err } func parseInvalidZid(val string) (any, error) { zid, _ := id.Parse(val) return zid, nil } |
Added internal/kernel/core.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "fmt" "maps" "net" "os" "runtime" "slices" "sync" "time" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/logger" "zettelstore.de/z/strfun" ) type coreService struct { srvConfig started bool mxRecover sync.RWMutex mapRecover map[string]recoverInfo } type recoverInfo struct { count uint64 ts time.Time info any stack []byte } func (cs *coreService) Initialize(logger *logger.Logger) { cs.logger = logger cs.mapRecover = make(map[string]recoverInfo) cs.descr = descriptionMap{ CoreDebug: {"Debug mode", parseBool, false}, CoreGoArch: {"Go processor architecture", nil, false}, CoreGoOS: {"Go Operating System", nil, false}, CoreGoVersion: {"Go Version", nil, false}, CoreHostname: {"Host name", nil, false}, CorePort: { "Port of command line server", cs.noFrozen(func(val string) (any, error) { port, err := net.LookupPort("tcp", val) if err != nil { return nil, err } return port, nil }), true, }, CoreProgname: {"Program name", nil, false}, CoreStarted: {"Start time", nil, false}, CoreVerbose: {"Verbose output", parseBool, true}, CoreVersion: { "Version", cs.noFrozen(func(val string) (any, error) { if val == "" { return CoreDefaultVersion, nil } return val, nil }), false, }, CoreVTime: {"Version time", nil, false}, } cs.next = interfaceMap{ CoreDebug: false, CoreGoArch: runtime.GOARCH, CoreGoOS: runtime.GOOS, CoreGoVersion: runtime.Version(), CoreHostname: "*unknown host*", CorePort: 0, CoreStarted: time.Now().Local().Format(id.TimestampLayout), CoreVerbose: false, } if hn, err := os.Hostname(); err == nil { cs.next[CoreHostname] = hn } } func (cs *coreService) GetLogger() *logger.Logger { return cs.logger } func (cs *coreService) Start(*Kernel) error { cs.started = true return nil } func (cs *coreService) IsStarted() bool { return cs.started } func (cs *coreService) Stop(*Kernel) { cs.started = false } func (cs *coreService) GetStatistics() []KeyValue { cs.mxRecover.RLock() defer cs.mxRecover.RUnlock() names := slices.Sorted(maps.Keys(cs.mapRecover)) result := make([]KeyValue, 0, 3*len(names)) for _, n := range names { ri := cs.mapRecover[n] result = append( result, KeyValue{ Key: fmt.Sprintf("Recover %q / Count", n), Value: fmt.Sprintf("%d", ri.count), }, KeyValue{ Key: fmt.Sprintf("Recover %q / Last ", n), Value: fmt.Sprintf("%v", ri.ts), }, KeyValue{ Key: fmt.Sprintf("Recover %q / Info ", n), Value: fmt.Sprintf("%v", ri.info), }, ) } return result } func (cs *coreService) RecoverLines(name string) []string { cs.mxRecover.RLock() ri := cs.mapRecover[name] cs.mxRecover.RUnlock() if ri.stack == nil { return nil } return append( []string{ fmt.Sprintf("Count: %d", ri.count), fmt.Sprintf("Time: %v", ri.ts), fmt.Sprintf("Reason: %v", ri.info), }, strfun.SplitLines(string(ri.stack))..., ) } func (cs *coreService) updateRecoverInfo(name string, recoverInfo any, stack []byte) { cs.mxRecover.Lock() ri := cs.mapRecover[name] ri.count++ ri.ts = time.Now().Local() ri.info = recoverInfo ri.stack = stack cs.mapRecover[name] = ri cs.mxRecover.Unlock() } |
Added internal/kernel/kernel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package kernel provides the main kernel service. package kernel import ( "errors" "fmt" "io" "net" "net/url" "os" "os/signal" "runtime" "runtime/debug" "runtime/pprof" "strconv" "strings" "sync" "syscall" "time" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/web/server" ) // Main references the main kernel. var Main *Kernel // Kernel is the main internal kernel. type Kernel struct { logWriter *kernelLogWriter logger *logger.Logger wg sync.WaitGroup mx sync.RWMutex interrupt chan os.Signal profileName string fileName string profileFile *os.File profile *pprof.Profile self kernelService core coreService cfg configService auth authService box boxService web webService srvs map[Service]serviceDescr srvNames map[string]serviceData depStart serviceDependency depStop serviceDependency // reverse of depStart } type serviceDescr struct { srv service name string logLevel logger.Level } type serviceData struct { srv service srvnum Service } type serviceDependency map[Service][]Service // create a new kernel. func init() { Main = createKernel() } // create a new func createKernel() *Kernel { lw := newKernelLogWriter(8192) kern := &Kernel{ logWriter: lw, logger: logger.New(lw, "").SetLevel(defaultNormalLogLevel), interrupt: make(chan os.Signal, 5), } kern.self.kernel = kern kern.srvs = map[Service]serviceDescr{ KernelService: {&kern.self, "kernel", defaultNormalLogLevel}, CoreService: {&kern.core, "core", defaultNormalLogLevel}, ConfigService: {&kern.cfg, "config", defaultNormalLogLevel}, AuthService: {&kern.auth, "auth", defaultNormalLogLevel}, BoxService: {&kern.box, "box", defaultNormalLogLevel}, WebService: {&kern.web, "web", defaultNormalLogLevel}, } kern.srvNames = make(map[string]serviceData, len(kern.srvs)) for key, srvD := range kern.srvs { if _, ok := kern.srvNames[srvD.name]; ok { kern.logger.Error().Str("service", srvD.name).Msg("Service data already set, ignore") } kern.srvNames[srvD.name] = serviceData{srvD.srv, key} l := logger.New(lw, strings.ToUpper(srvD.name)).SetLevel(srvD.logLevel) kern.logger.Debug().Str("service", srvD.name).Msg("Initialize") srvD.srv.Initialize(l) } kern.depStart = serviceDependency{ KernelService: nil, CoreService: {KernelService}, ConfigService: {CoreService}, AuthService: {CoreService}, BoxService: {CoreService, ConfigService, AuthService}, WebService: {ConfigService, AuthService, BoxService}, } kern.depStop = make(serviceDependency, len(kern.depStart)) for srv, deps := range kern.depStart { for _, dep := range deps { kern.depStop[dep] = append(kern.depStop[dep], srv) } } return kern } // Constants for profile names. const ( ProfileCPU = "CPU" ProfileHead = "heap" ) // Service specifies a service, e.g. web, ... type Service uint8 // Constants for type Service. const ( _ Service = iota KernelService // The Kernel itself is also a sevice CoreService // Manages startup specific functionality ConfigService // Provides access to runtime configuration AuthService // Manages authentication BoxService // Boxes provide zettel WebService // Access to Zettelstore through Web-based API and WebUI ) // Constants for core service system keys. const ( CoreDebug = "debug" CoreGoArch = "go-arch" CoreGoOS = "go-os" CoreGoVersion = "go-version" CoreHostname = "hostname" CorePort = "port" CoreProgname = "progname" CoreStarted = "started" CoreVerbose = "verbose" CoreVersion = "version" CoreVTime = "vtime" ) // Defined values for core service. const ( CoreDefaultVersion = "unknown" ) // Constants for config service keys. const ( ConfigSimpleMode = "simple-mode" ConfigInsecureHTML = "insecure-html" ) // Constants for authentication service keys. const ( AuthOwner = "owner" AuthReadonly = "readonly" ) // Constants for box service keys. const ( BoxDefaultDirType = "defdirtype" BoxURIs = "box-uri-" ) // Allowed values for BoxDefaultDirType const ( BoxDirTypeNotify = "notify" BoxDirTypeSimple = "simple" ) // Constants for config service keys. const ( ConfigSecureHTML = "secure" ConfigSyntaxHTML = "html" ConfigMarkdownHTML = "markdown" ConfigZmkHTML = "zettelmarkup" ) // Constants for web service keys. const ( WebAssetDir = "asset-dir" WebBaseURL = "base-url" WebListenAddress = "listen" WebPersistentCookie = "persistent" WebProfiling = "profiling" WebMaxRequestSize = "max-request-size" WebSecureCookie = "secure" WebTokenLifetimeAPI = "api-lifetime" WebTokenLifetimeHTML = "html-lifetime" WebURLPrefix = "prefix" ) // KeyDescrValue is a triple of config data. type KeyDescrValue struct{ Key, Descr, Value string } // KeyValue is a pair of key and value. type KeyValue struct{ Key, Value string } // LogEntry stores values of one log line written by a logger.Logger type LogEntry struct { Level logger.Level TS time.Time Prefix string Message string } // CreateAuthManagerFunc is called to create a new auth manager. type CreateAuthManagerFunc func(readonly bool, owner id.Zid) (auth.Manager, error) // CreateBoxManagerFunc is called to create a new box manager. type CreateBoxManagerFunc func( boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config, ) (box.Manager, error) // SetupWebServerFunc is called to create a new web service handler. type SetupWebServerFunc func( webServer server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config, ) error const ( defaultNormalLogLevel = logger.InfoLevel defaultSimpleLogLevel = logger.ErrorLevel ) // Setup sets the most basic data of a software: its name, its version, // and when the version was created. func (kern *Kernel) Setup(progname, version string, versionTime time.Time) { kern.SetConfig(CoreService, CoreProgname, progname) kern.SetConfig(CoreService, CoreVersion, version) kern.SetConfig(CoreService, CoreVTime, versionTime.Local().Format(id.TimestampLayout)) } // Start the service. func (kern *Kernel) Start(headline, lineServer bool, configFilename string) { for _, srvD := range kern.srvs { srvD.srv.Freeze() } if kern.cfg.GetCurConfig(ConfigSimpleMode).(bool) { kern.SetLogLevel(defaultSimpleLogLevel.String()) } kern.wg.Add(1) signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM) go func() { // Wait for interrupt. sig := <-kern.interrupt if strSig := sig.String(); strSig != "" { kern.logger.Info().Str("signal", strSig).Msg("Shut down Zettelstore") } kern.doShutdown() kern.wg.Done() }() kern.StartService(KernelService) if headline { logger := kern.logger logger.Mandatory().Msg(fmt.Sprintf( "%v %v (%v@%v/%v)", kern.core.GetCurConfig(CoreProgname), kern.core.GetCurConfig(CoreVersion), kern.core.GetCurConfig(CoreGoVersion), kern.core.GetCurConfig(CoreGoOS), kern.core.GetCurConfig(CoreGoArch), )) logger.Mandatory().Msg("Licensed under the latest version of the EUPL (European Union Public License)") if configFilename != "" { logger.Mandatory().Str("filename", configFilename).Msg("Configuration file found") } else { logger.Mandatory().Msg("No configuration file found / used") } if kern.core.GetCurConfig(CoreDebug).(bool) { logger.Info().Msg("----------------------------------------") logger.Info().Msg("DEBUG MODE, DO NO USE THIS IN PRODUCTION") logger.Info().Msg("----------------------------------------") } if kern.auth.GetCurConfig(AuthReadonly).(bool) { logger.Info().Msg("Read-only mode") } } if lineServer { port := kern.core.GetNextConfig(CorePort).(int) if port > 0 { listenAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) startLineServer(kern, listenAddr) } } } func (kern *Kernel) doShutdown() { kern.stopService(KernelService) // Will stop all other services. } // WaitForShutdown blocks the call until Shutdown is called. func (kern *Kernel) WaitForShutdown() { kern.wg.Wait() kern.doStopProfiling() } // --- Shutdown operation ---------------------------------------------------- // Shutdown the service. Waits for all concurrent activities to stop. func (kern *Kernel) Shutdown(silent bool) { kern.logger.Trace().Msg("Shutdown") kern.interrupt <- &shutdownSignal{silent: silent} } type shutdownSignal struct{ silent bool } func (s *shutdownSignal) String() string { if s.silent { return "" } return "shutdown" } func (*shutdownSignal) Signal() { /* Just a signal */ } // --- Log operation --------------------------------------------------------- // GetKernelLogger returns the kernel logger. func (kern *Kernel) GetKernelLogger() *logger.Logger { return kern.logger } // SetLogLevel sets the logging level for logger maintained by the kernel. // // Its syntax is: (SERVICE ":")? LEVEL (";" (SERICE ":")? LEVEL)*. func (kern *Kernel) SetLogLevel(logLevel string) { defaultLevel, srvLevel := kern.parseLogLevel(logLevel) kern.mx.RLock() defer kern.mx.RUnlock() for srvN, srvD := range kern.srvs { if lvl, found := srvLevel[srvN]; found { srvD.srv.GetLogger().SetLevel(lvl) } else if defaultLevel != logger.NoLevel { srvD.srv.GetLogger().SetLevel(defaultLevel) } } } func (kern *Kernel) parseLogLevel(logLevel string) (logger.Level, map[Service]logger.Level) { defaultLevel := logger.NoLevel srvLevel := map[Service]logger.Level{} for spec := range strings.SplitSeq(logLevel, ";") { vals := cleanLogSpec(strings.Split(spec, ":")) switch len(vals) { case 0: case 1: if lvl := logger.ParseLevel(vals[0]); lvl.IsValid() { defaultLevel = lvl } default: serviceText, levelText := vals[0], vals[1] if srv, found := kern.srvNames[serviceText]; found { if lvl := logger.ParseLevel(levelText); lvl.IsValid() { srvLevel[srv.srvnum] = lvl } } } } return defaultLevel, srvLevel } func cleanLogSpec(rawVals []string) []string { vals := make([]string, 0, len(rawVals)) for _, rVal := range rawVals { val := strings.TrimSpace(rVal) if val != "" { vals = append(vals, val) } } return vals } // RetrieveLogEntries returns all buffered log entries. func (kern *Kernel) RetrieveLogEntries() []LogEntry { return kern.logWriter.retrieveLogEntries() } // GetLastLogTime returns the time when the last logging with level > DEBUG happened. func (kern *Kernel) GetLastLogTime() time.Time { return kern.logWriter.getLastLogTime() } // LogRecover outputs some information about the previous panic. func (kern *Kernel) LogRecover(name string, recoverInfo any) { stack := debug.Stack() kern.logger.Error().Str("recovered_from", fmt.Sprint(recoverInfo)).Bytes("stack", stack).Msg(name) kern.core.updateRecoverInfo(name, recoverInfo, stack) } // --- Profiling --------------------------------------------------------- var errProfileInWork = errors.New("already profiling") var errProfileNotFound = errors.New("profile not found") // StartProfiling starts profiling the software according to a profile. // It is an error to start more than one profile. // // profileName is a valid profile (see runtime/pprof/Lookup()), or the // value "cpu" for profiling the CPI. // fileName is the name of the file where the results are written to. func (kern *Kernel) StartProfiling(profileName, fileName string) error { kern.mx.Lock() defer kern.mx.Unlock() return kern.doStartProfiling(profileName, fileName) } func (kern *Kernel) doStartProfiling(profileName, fileName string) error { if kern.profileName != "" { return errProfileInWork } if profileName == ProfileCPU { f, err := os.Create(fileName) if err != nil { return err } err = pprof.StartCPUProfile(f) if err != nil { f.Close() return err } kern.profileName = profileName kern.fileName = fileName kern.profileFile = f return nil } profile := pprof.Lookup(profileName) if profile == nil { return errProfileNotFound } f, err := os.Create(fileName) if err != nil { return err } kern.profileName = profileName kern.fileName = fileName kern.profile = profile kern.profileFile = f runtime.GC() // get up-to-date statistics profile.WriteTo(f, 0) return nil } // StopProfiling stops the current profiling and writes the result to // the file, which was named during StartProfiling(). // It will always be called before the software stops its operations. func (kern *Kernel) StopProfiling() error { kern.mx.Lock() defer kern.mx.Unlock() return kern.doStopProfiling() } func (kern *Kernel) doStopProfiling() error { if kern.profileName == "" { return nil // No profile started } if kern.profileName == ProfileCPU { pprof.StopCPUProfile() } err := kern.profileFile.Close() kern.profileName = "" kern.fileName = "" kern.profile = nil kern.profileFile = nil return err } // --- Service handling -------------------------------------------------- var errUnknownService = errors.New("unknown service") // SetConfig stores a configuration value. func (kern *Kernel) SetConfig(srvnum Service, key, value string) error { kern.mx.Lock() defer kern.mx.Unlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.SetConfig(key, value) } return errUnknownService } // GetConfig returns a configuration value. func (kern *Kernel) GetConfig(srvnum Service, key string) any { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetCurConfig(key) } return nil } // GetServiceStatistics returns a key/value list with statistical data. func (kern *Kernel) GetServiceStatistics(srvnum Service) []KeyValue { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetStatistics() } return nil } // GetLogger returns a logger for the given service. func (kern *Kernel) GetLogger(srvnum Service) *logger.Logger { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetLogger() } return kern.GetKernelLogger() } // StartService start the given service. func (kern *Kernel) StartService(srvnum Service) error { kern.mx.RLock() defer kern.mx.RUnlock() return kern.doStartService(srvnum) } func (kern *Kernel) doStartService(srvnum Service) error { for _, srv := range kern.sortDependency(srvnum, kern.depStart, true) { if err := srv.Start(kern); err != nil { return err } srv.SwitchNextToCur() } return nil } // restartService stops and restarts the given service, while maintaining service dependencies. func (kern *Kernel) restartService(srvnum Service) error { deps := kern.sortDependency(srvnum, kern.depStop, false) for _, srv := range deps { srv.Stop(kern) } for i := len(deps) - 1; i >= 0; i-- { srv := deps[i] if err := srv.Start(kern); err != nil { return err } srv.SwitchNextToCur() } return nil } // stopService stop the given service. func (kern *Kernel) stopService(srvnum Service) { kern.mx.Lock() defer kern.mx.Unlock() kern.doStopService(srvnum) } func (kern *Kernel) doStopService(srvnum Service) { for _, srv := range kern.sortDependency(srvnum, kern.depStop, false) { srv.Stop(kern) } } func (kern *Kernel) sortDependency( srvnum Service, srvdeps serviceDependency, isStarted bool, ) []service { srvD, ok := kern.srvs[srvnum] if !ok { return nil } if srvD.srv.IsStarted() == isStarted { return nil } deps := srvdeps[srvnum] found := make(map[service]bool, 8) result := make([]service, 0, len(found)) for _, dep := range deps { srvDeps := kern.sortDependency(dep, srvdeps, isStarted) for _, depSrv := range srvDeps { if !found[depSrv] { result = append(result, depSrv) found[depSrv] = true } } } return append(result, srvD.srv) } // dumpIndex writes some data about the internal index into a writer. func (kern *Kernel) dumpIndex(w io.Writer) { kern.box.dumpIndex(w) } type service interface { // Initialize the data for the service. Initialize(*logger.Logger) // Get service logger. GetLogger() *logger.Logger // ConfigDescriptions returns a sorted list of configuration descriptions. ConfigDescriptions() []serviceConfigDescription // SetConfig stores a configuration value. SetConfig(key, value string) error // GetCurConfig returns the current configuration value. GetCurConfig(key string) any // GetNextConfig returns the next configuration value. GetNextConfig(key string) any // GetCurConfigList returns a sorted list of current configuration data. GetCurConfigList(all bool) []KeyDescrValue // GetNextConfigList returns a sorted list of next configuration data. GetNextConfigList() []KeyDescrValue // GetStatistics returns a key/value list of statistical data. GetStatistics() []KeyValue // Freeze disallows to change some fixed configuration values. Freeze() // Start the service. Start(*Kernel) error // SwitchNextToCur moves next config data to current. SwitchNextToCur() // IsStarted returns true if the service was started successfully. IsStarted() bool // Stop the service. Stop(*Kernel) } type serviceConfigDescription struct{ Key, Descr string } // SetCreators store functions to be called when a service has to be created. func (kern *Kernel) SetCreators( createAuthManager CreateAuthManagerFunc, createBoxManager CreateBoxManagerFunc, setupWebServer SetupWebServerFunc, ) { kern.auth.createManager = createAuthManager kern.box.createManager = createBoxManager kern.web.setupServer = setupWebServer } // --- The kernel as a service ------------------------------------------- type kernelService struct { kernel *Kernel } func (*kernelService) Initialize(*logger.Logger) {} func (ks *kernelService) GetLogger() *logger.Logger { return ks.kernel.logger } func (*kernelService) ConfigDescriptions() []serviceConfigDescription { return nil } func (*kernelService) SetConfig(string, string) error { return errAlreadyFrozen } func (*kernelService) GetCurConfig(string) any { return nil } func (*kernelService) GetNextConfig(string) any { return nil } func (*kernelService) GetCurConfigList(bool) []KeyDescrValue { return nil } func (*kernelService) GetNextConfigList() []KeyDescrValue { return nil } func (*kernelService) GetStatistics() []KeyValue { return nil } func (*kernelService) Freeze() {} func (*kernelService) Start(*Kernel) error { return nil } func (*kernelService) SwitchNextToCur() {} func (*kernelService) IsStarted() bool { return true } func (*kernelService) Stop(*Kernel) {} |
Added internal/kernel/log.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "os" "slices" "sync" "time" "zettelstore.de/z/internal/logger" ) // kernelLogWriter adapts an io.Writer to a LogWriter type kernelLogWriter struct { mx sync.RWMutex // protects buf, serializes w.Write and retrieveLogEntries lastLog time.Time buf []byte writePos int data []logEntry full bool } // newKernelLogWriter creates a new LogWriter for kernel logging. func newKernelLogWriter(capacity int) *kernelLogWriter { if capacity < 1 { capacity = 1 } return &kernelLogWriter{ lastLog: time.Now(), buf: make([]byte, 0, 500), data: make([]logEntry, capacity), } } func (klw *kernelLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error { klw.mx.Lock() if level > logger.DebugLevel { klw.lastLog = ts klw.data[klw.writePos] = logEntry{ level: level, ts: ts, prefix: prefix, msg: msg, details: slices.Clone(details), } klw.writePos++ if klw.writePos >= cap(klw.data) { klw.writePos = 0 klw.full = true } } klw.buf = klw.buf[:0] buf := klw.buf addTimestamp(&buf, ts) buf = append(buf, ' ') buf = append(buf, level.Format()...) buf = append(buf, ' ') if prefix != "" { buf = append(buf, prefix...) buf = append(buf, ' ') } buf = append(buf, msg...) buf = append(buf, details...) buf = append(buf, '\n') _, err := os.Stdout.Write(buf) klw.mx.Unlock() return err } func addTimestamp(buf *[]byte, ts time.Time) { year, month, day := ts.Date() itoa(buf, year, 4) *buf = append(*buf, '-') itoa(buf, int(month), 2) *buf = append(*buf, '-') itoa(buf, day, 2) *buf = append(*buf, ' ') hour, minute, second := ts.Clock() itoa(buf, hour, 2) *buf = append(*buf, ':') itoa(buf, minute, 2) *buf = append(*buf, ':') itoa(buf, second, 2) } func itoa(buf *[]byte, i, wid int) { var b [20]byte for bp := wid - 1; bp >= 0; bp-- { q := i / 10 b[bp] = byte('0' + i - q*10) i = q } *buf = append(*buf, b[:wid]...) } type logEntry struct { level logger.Level ts time.Time prefix string msg string details []byte } func (klw *kernelLogWriter) retrieveLogEntries() []LogEntry { klw.mx.RLock() defer klw.mx.RUnlock() if !klw.full { if klw.writePos == 0 { return nil } result := make([]LogEntry, klw.writePos) for i := range klw.writePos { copyE2E(&result[i], &klw.data[i]) } return result } result := make([]LogEntry, cap(klw.data)) pos := 0 for j := klw.writePos; j < cap(klw.data); j++ { copyE2E(&result[pos], &klw.data[j]) pos++ } for j := range klw.writePos { copyE2E(&result[pos], &klw.data[j]) pos++ } return result } func (klw *kernelLogWriter) getLastLogTime() time.Time { klw.mx.RLock() defer klw.mx.RUnlock() return klw.lastLog } func copyE2E(result *LogEntry, origin *logEntry) { result.Level = origin.level result.TS = origin.ts result.Prefix = origin.prefix result.Message = origin.msg + string(origin.details) } |
Added internal/kernel/server.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "bufio" "net" ) func startLineServer(kern *Kernel, listenAddr string) error { ln, err := net.Listen("tcp", listenAddr) if err != nil { kern.logger.Error().Err(err).Msg("Unable to start administration console") return err } kern.logger.Mandatory().Str("listen", listenAddr).Msg("Start administration console") go func() { lineServer(ln, kern) }() return nil } func lineServer(ln net.Listener, kern *Kernel) { // Something may panic. Ensure a running line service. defer func() { if ri := recover(); ri != nil { kern.LogRecover("Line", ri) go lineServer(ln, kern) } }() for { conn, err := ln.Accept() if err != nil { // handle error kern.logger.Error().Err(err).Msg("Unable to accept connection") break } go handleLineConnection(conn, kern) } ln.Close() } func handleLineConnection(conn net.Conn, kern *Kernel) { // Something may panic. Ensure a running connection. defer func() { if ri := recover(); ri != nil { kern.LogRecover("LineConn", ri) go handleLineConnection(conn, kern) } }() kern.logger.Mandatory().Str("from", conn.RemoteAddr().String()).Msg("Start session on administration console") cmds := cmdSession{} cmds.initialize(conn, kern) s := bufio.NewScanner(conn) for s.Scan() { line := s.Text() if !cmds.executeLine(line) { break } } conn.Close() } |
Added internal/kernel/web.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "errors" "net" "net/netip" "net/url" "os" "path/filepath" "strconv" "strings" "sync" "time" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/web/server" ) type webService struct { srvConfig mxService sync.RWMutex srvw server.Server setupServer SetupWebServerFunc } var errURLPrefixSyntax = errors.New("must not be empty and must start with '//'") func (ws *webService) Initialize(logger *logger.Logger) { ws.logger = logger ws.descr = descriptionMap{ WebAssetDir: { "Asset file directory", func(val string) (any, error) { val = filepath.Clean(val) finfo, err := os.Stat(val) if err == nil && finfo.IsDir() { return val, nil } return nil, err }, true, }, WebBaseURL: { "Base URL", func(val string) (any, error) { if _, err := url.Parse(val); err != nil { return nil, err } return val, nil }, true, }, WebListenAddress: { "Listen address", func(val string) (any, error) { // If there is no host, prepend 127.0.0.1 as host. host, _, err := net.SplitHostPort(val) if err == nil && host == "" { val = "127.0.0.1" + val } ap, err := netip.ParseAddrPort(val) if err != nil { return "", err } return ap.String(), nil }, true}, WebMaxRequestSize: {"Max Request Size", parseInt64, true}, WebPersistentCookie: {"Persistent cookie", parseBool, true}, WebProfiling: {"Runtime profiling", parseBool, true}, WebSecureCookie: {"Secure cookie", parseBool, true}, WebTokenLifetimeAPI: { "Token lifetime API", makeDurationParser(10*time.Minute, 0, 1*time.Hour), true, }, WebTokenLifetimeHTML: { "Token lifetime HTML", makeDurationParser(1*time.Hour, 1*time.Minute, 30*24*time.Hour), true, }, WebURLPrefix: { "URL prefix under which the web server runs", func(val string) (any, error) { if val != "" && val[0] == '/' && val[len(val)-1] == '/' { return val, nil } return nil, errURLPrefixSyntax }, true, }, } ws.next = interfaceMap{ WebAssetDir: "", WebBaseURL: "http://127.0.0.1:23123/", WebListenAddress: "127.0.0.1:23123", WebMaxRequestSize: int64(16 * 1024 * 1024), WebPersistentCookie: false, WebSecureCookie: true, WebProfiling: false, WebTokenLifetimeAPI: 1 * time.Hour, WebTokenLifetimeHTML: 10 * time.Minute, WebURLPrefix: "/", } } func makeDurationParser(defDur, minDur, maxDur time.Duration) parseFunc { return func(val string) (any, error) { if d, err := strconv.ParseUint(val, 10, 64); err == nil { secs := time.Duration(d) * time.Minute if secs < minDur { return minDur, nil } if secs > maxDur { return maxDur, nil } return secs, nil } return defDur, nil } } var errWrongBasePrefix = errors.New(WebURLPrefix + " does not match " + WebBaseURL) func (ws *webService) GetLogger() *logger.Logger { return ws.logger } func (ws *webService) Start(kern *Kernel) error { baseURL := ws.GetNextConfig(WebBaseURL).(string) listenAddr := ws.GetNextConfig(WebListenAddress).(string) urlPrefix := ws.GetNextConfig(WebURLPrefix).(string) persistentCookie := ws.GetNextConfig(WebPersistentCookie).(bool) secureCookie := ws.GetNextConfig(WebSecureCookie).(bool) profile := ws.GetNextConfig(WebProfiling).(bool) maxRequestSize := max(ws.GetNextConfig(WebMaxRequestSize).(int64), 1024) if !strings.HasSuffix(baseURL, urlPrefix) { ws.logger.Error().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg( "url-prefix is not a suffix of base-url") return errWrongBasePrefix } if lap := netip.MustParseAddrPort(listenAddr); !kern.auth.manager.WithAuth() && !lap.Addr().IsLoopback() { ws.logger.Info().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled") } sd := server.ConfigData{ Log: ws.logger, ListenAddr: listenAddr, BaseURL: baseURL, URLPrefix: urlPrefix, MaxRequestSize: maxRequestSize, Auth: kern.auth.manager, PersistentCookie: persistentCookie, SecureCookie: secureCookie, Profiling: profile, } srvw := server.New(sd) err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg) if err != nil { ws.logger.Error().Err(err).Msg("Unable to create") return err } if err = srvw.Run(); err != nil { ws.logger.Error().Err(err).Msg("Unable to start") return err } ws.logger.Info().Str("listen", listenAddr).Str("base-url", baseURL).Msg("Start Service") ws.mxService.Lock() ws.srvw = srvw ws.mxService.Unlock() if kern.cfg.GetCurConfig(ConfigSimpleMode).(bool) { listenAddr := ws.GetNextConfig(WebListenAddress).(string) if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 { ws.logger.Mandatory().Msg(strings.Repeat("--------------------", 3)) ws.logger.Mandatory().Msg("Open your browser and enter the following URL:") ws.logger.Mandatory().Msg(" http://localhost" + listenAddr[idx:]) ws.logger.Mandatory().Msg("") ws.logger.Mandatory().Msg("If this does not work, try:") ws.logger.Mandatory().Msg(" http://127.0.0.1" + listenAddr[idx:]) } } return nil } func (ws *webService) IsStarted() bool { ws.mxService.RLock() defer ws.mxService.RUnlock() return ws.srvw != nil } func (ws *webService) Stop(*Kernel) { ws.logger.Info().Msg("Stop Service") ws.srvw.Stop() ws.mxService.Lock() ws.srvw = nil ws.mxService.Unlock() } func (*webService) GetStatistics() []KeyValue { return nil } |
Added internal/logger/logger.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package logger implements a logging package for use in the Zettelstore. package logger import ( "context" "strconv" "strings" "sync/atomic" "time" "t73f.de/r/zsc/domain/meta" ) // Level defines the possible log levels type Level uint8 // Constants for Level const ( NoLevel Level = iota // the absent log level TraceLevel // Log most internal activities DebugLevel // Log most data updates InfoLevel // Log normal activities ErrorLevel // Log (persistent) errors MandatoryLevel // Log only mandatory events NeverLevel // Logging is disabled ) var logLevel = [...]string{ " ", "TRACE", "DEBUG", "INFO ", "ERROR", ">>>>>", "NEVER", } var strLevel = [...]string{ "", "trace", "debug", "info", "error", "mandatory", "disabled", } // IsValid returns true, if the level is a valid level func (l Level) IsValid() bool { return TraceLevel <= l && l <= NeverLevel } func (l Level) String() string { if l.IsValid() { return strLevel[l] } return strconv.Itoa(int(l)) } // Format returns a string representation suitable for logging. func (l Level) Format() string { if l.IsValid() { return logLevel[l] } return strconv.Itoa(int(l)) } // ParseLevel returns the recognized level. func ParseLevel(text string) Level { for lv := TraceLevel; lv <= NeverLevel; lv++ { if len(text) > 2 && strings.HasPrefix(strLevel[lv], text) { return lv } } return NoLevel } // Logger represents an objects that emits logging messages. type Logger struct { lw LogWriter levelVal uint32 prefix string context []byte topParent *Logger uProvider UserProvider } // LogWriter writes log messages to their specified destinations. type LogWriter interface { WriteMessage(level Level, ts time.Time, prefix, msg string, details []byte) error } // New creates a new logger for the given service. // // This function must only be called from a kernel implementation, not from // code that tries to log something. func New(lw LogWriter, prefix string) *Logger { if prefix != "" && len(prefix) < 6 { prefix = (prefix + " ")[:6] } result := &Logger{ lw: lw, levelVal: uint32(InfoLevel), prefix: prefix, context: nil, uProvider: nil, } result.topParent = result return result } func newFromMessage(msg *Message) *Logger { if msg == nil { return nil } logger := msg.logger context := make([]byte, 0, len(msg.buf)) context = append(context, msg.buf...) return &Logger{ lw: nil, levelVal: 0, prefix: logger.prefix, context: context, topParent: logger.topParent, uProvider: nil, } } // SetLevel sets the level of the logger. func (l *Logger) SetLevel(newLevel Level) *Logger { if l != nil { if l.topParent != l { panic("try to set level for child logger") } atomic.StoreUint32(&l.levelVal, uint32(newLevel)) } return l } // Level returns the current level of the given logger func (l *Logger) Level() Level { if l != nil { return Level(atomic.LoadUint32(&l.levelVal)) } return NeverLevel } // Trace creates a tracing message. func (l *Logger) Trace() *Message { return newMessage(l, TraceLevel) } // Debug creates a debug message. func (l *Logger) Debug() *Message { return newMessage(l, DebugLevel) } // Info creates a message suitable for information data. func (l *Logger) Info() *Message { return newMessage(l, InfoLevel) } // Error creates a message suitable for errors. func (l *Logger) Error() *Message { return newMessage(l, ErrorLevel) } // Mandatory creates a message that will always logged, except when logging // is disabled. func (l *Logger) Mandatory() *Message { return newMessage(l, MandatoryLevel) } // Clone creates a message to clone the logger. func (l *Logger) Clone() *Message { msg := newMessage(l, NeverLevel) if msg != nil { msg.level = NoLevel } return msg } // UserProvider allows to retrieve an user metadata from a context. type UserProvider interface { GetUser(ctx context.Context) *meta.Meta } // WithUser creates a derivied logger that allows to retrieve and log user identifer. func (l *Logger) WithUser(up UserProvider) *Logger { return &Logger{ lw: nil, levelVal: 0, prefix: l.prefix, context: l.context, topParent: l.topParent, uProvider: up, } } func (l *Logger) writeMessage(level Level, msg string, details []byte) error { return l.topParent.lw.WriteMessage(level, time.Now().Local(), l.prefix, msg, details) } |
Added internal/logger/logger_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package logger_test import ( "fmt" "os" "testing" "time" "zettelstore.de/z/internal/logger" ) func TestParseLevel(t *testing.T) { testcases := []struct { text string exp logger.Level }{ {"tra", logger.TraceLevel}, {"deb", logger.DebugLevel}, {"info", logger.InfoLevel}, {"err", logger.ErrorLevel}, {"manda", logger.MandatoryLevel}, {"dis", logger.NeverLevel}, {"d", logger.Level(0)}, } for i, tc := range testcases { got := logger.ParseLevel(tc.text) if got != tc.exp { t.Errorf("%d: ParseLevel(%q) == %q, but got %q", i, tc.text, tc.exp, got) } } } func BenchmarkDisabled(b *testing.B) { log := logger.New(&stderrLogWriter{}, "").SetLevel(logger.NeverLevel) for b.Loop() { log.Info().Str("key", "val").Msg("Benchmark") } } type stderrLogWriter struct{} func (*stderrLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error { fmt.Fprintf(os.Stderr, "%v %v %v %v %v\n", level.Format(), ts, prefix, msg, string(details)) return nil } type testLogWriter struct{} func (*testLogWriter) WriteMessage(logger.Level, time.Time, string, string, []byte) error { return nil } func BenchmarkStrMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "") for b.Loop() { log.Info().Str("key", "val").Msg("Benchmark") } } func BenchmarkMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "") for b.Loop() { log.Info().Msg("Benchmark") } } func BenchmarkCloneStrMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "").Clone().Str("sss", "ttt").Child() for b.Loop() { log.Info().Msg("123456789") } } |
Added internal/logger/message.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package logger import ( "context" "net/http" "strconv" "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" ) // Message presents a message to log. type Message struct { logger *Logger level Level buf []byte } func newMessage(logger *Logger, level Level) *Message { if logger != nil { if logger.topParent.Level() <= level { m := messagePool.Get().(*Message) m.logger = logger m.level = level m.buf = append(m.buf[:0], logger.context...) return m } } return nil } func recycleMessage(m *Message) { messagePool.Put(m) } var messagePool = &sync.Pool{ New: func() any { return &Message{ buf: make([]byte, 0, 500), } }, } // Enabled returns whether the message will log or not. func (m *Message) Enabled() bool { return m != nil && m.level != NeverLevel } // Str adds a string value to the full message func (m *Message) Str(text, val string) *Message { if m.Enabled() { buf := append(m.buf, ',', ' ') buf = append(buf, text...) buf = append(buf, '=') m.buf = append(buf, val...) } return m } // Bool adds a boolean value to the full message func (m *Message) Bool(text string, val bool) *Message { if val { m.Str(text, "true") } else { m.Str(text, "false") } return m } // Bytes adds a byte slice value to the full message func (m *Message) Bytes(text string, val []byte) *Message { if m.Enabled() { buf := append(m.buf, ',', ' ') buf = append(buf, text...) buf = append(buf, '=') m.buf = append(buf, val...) } return m } // Err adds an error value to the full message func (m *Message) Err(err error) *Message { if err != nil { return m.Str("error", err.Error()) } return m } // Int adds an integer to the full message func (m *Message) Int(text string, i int64) *Message { return m.Str(text, strconv.FormatInt(i, 10)) } // Uint adds an unsigned integer to the full message func (m *Message) Uint(text string, u uint64) *Message { return m.Str(text, strconv.FormatUint(u, 10)) } // User adds the user-id field of the given user to the message. func (m *Message) User(ctx context.Context) *Message { if m.Enabled() { if up := m.logger.uProvider; up != nil { if user := up.GetUser(ctx); user != nil { m.buf = append(m.buf, ", user="...) if userID, found := user.Get(meta.KeyUserID); found { m.buf = append(m.buf, userID...) } else { m.buf = append(m.buf, user.Zid.Bytes()...) } } } } return m } // HTTPIP adds the IP address of a HTTP request to the message. func (m *Message) HTTPIP(r *http.Request) *Message { if r == nil { return m } if from := r.Header.Get("X-Forwarded-For"); from != "" { return m.Str("ip", from) } return m.Str("IP", r.RemoteAddr) } // Zid adds a zettel identifier to the full message func (m *Message) Zid(zid id.Zid) *Message { return m.Bytes("zid", zid.Bytes()) } // Msg add the given text to the message and writes it to the log. func (m *Message) Msg(text string) { if m.Enabled() { m.logger.writeMessage(m.level, text, m.buf) recycleMessage(m) } } // Child creates a child logger with context of this message. func (m *Message) Child() *Logger { return newFromMessage(m) } |
Added internal/parser/blob.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package parser // blob provides a parser of binary data. import ( "t73f.de/r/sx" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx" "t73f.de/r/zsx/input" ) func init() { register(&Info{ Name: meta.ValueSyntaxGif, AltNames: nil, IsASTParser: false, IsTextFormat: false, IsImageFormat: true, Parse: parseBlob, }) register(&Info{ Name: meta.ValueSyntaxJPEG, AltNames: []string{meta.ValueSyntaxJPG}, IsASTParser: false, IsTextFormat: false, IsImageFormat: true, Parse: parseBlob, }) register(&Info{ Name: meta.ValueSyntaxPNG, AltNames: nil, IsASTParser: false, IsTextFormat: false, IsImageFormat: true, Parse: parseBlob, }) register(&Info{ Name: meta.ValueSyntaxWebp, AltNames: nil, IsASTParser: false, IsTextFormat: false, IsImageFormat: true, Parse: parseBlob, }) } func parseBlob(inp *input.Input, m *meta.Meta, syntax string) *sx.Pair { if p := Get(syntax); p != nil { syntax = p.Name } return zsx.MakeBlock(zsx.MakeBLOB(nil, ParseDescription(m), syntax, string(inp.Src))) } |
Added internal/parser/cleaner.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package parser // cleaner provides functions to clean up the parsed AST. import ( "strconv" "strings" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/strfun" ) // Clean cleans the given block list. func Clean(bs *ast.BlockSlice, allowHTML bool) { cv := cleanVisitor{ allowHTML: allowHTML, hasMark: false, doMark: false, } ast.Walk(&cv, bs) if cv.hasMark { cv.doMark = true ast.Walk(&cv, bs) } } type cleanVisitor struct { textEnc encoder.TextEncoder ids map[string]ast.Node allowHTML bool hasMark bool doMark bool } func (cv *cleanVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: if !cv.allowHTML { cv.visitBlockSlice(n) return nil } case *ast.InlineSlice: if !cv.allowHTML { cv.visitInlineSlice(n) return nil } case *ast.HeadingNode: cv.visitHeading(n) return nil case *ast.MarkNode: cv.visitMark(n) return nil } return cv } func (cv *cleanVisitor) visitBlockSlice(bs *ast.BlockSlice) { if bs == nil { return } if len(*bs) == 0 { *bs = nil return } for _, bn := range *bs { ast.Walk(cv, bn) } fromPos, toPos := 0, 0 for fromPos < len(*bs) { (*bs)[toPos] = (*bs)[fromPos] fromPos++ switch bn := (*bs)[toPos].(type) { case *ast.VerbatimNode: if bn.Kind != ast.VerbatimHTML { toPos++ } default: toPos++ } } for pos := toPos; pos < len(*bs); pos++ { (*bs)[pos] = nil // Allow excess nodes to be garbage collected. } *bs = (*bs)[:toPos:toPos] } func (cv *cleanVisitor) visitInlineSlice(is *ast.InlineSlice) { if is == nil { return } if len(*is) == 0 { *is = nil return } for _, bn := range *is { ast.Walk(cv, bn) } } func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) { if cv.doMark || hn == nil || len(hn.Inlines) == 0 { return } if hn.Slug == "" { var sb strings.Builder _, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines) if err != nil { return } hn.Slug = strfun.Slugify(sb.String()) } if hn.Slug != "" { hn.Fragment = cv.addIdentifier(hn.Slug, hn) } } func (cv *cleanVisitor) visitMark(mn *ast.MarkNode) { if !cv.doMark { cv.hasMark = true return } if mn.Mark == "" { mn.Slug = "" mn.Fragment = cv.addIdentifier("*", mn) return } if mn.Slug == "" { mn.Slug = strfun.Slugify(mn.Mark) } mn.Fragment = cv.addIdentifier(mn.Slug, mn) } func (cv *cleanVisitor) addIdentifier(id string, node ast.Node) string { if cv.ids == nil { cv.ids = map[string]ast.Node{id: node} return id } if n, ok := cv.ids[id]; ok && n != node { prefix := id + "-" for count := 1; ; count++ { newID := prefix + strconv.Itoa(count) if n2, ok2 := cv.ids[newID]; !ok2 || n2 == node { cv.ids[newID] = node return newID } } } cv.ids[id] = node return id } // CleanInlineLinks removes all links and footnote node from the given inline slice. func CleanInlineLinks(is *ast.InlineSlice) { ast.Walk(&cleanLinks{}, is) } type cleanLinks struct{} func (cl *cleanLinks) Visit(node ast.Node) ast.Visitor { ins, ok := node.(*ast.InlineSlice) if !ok { return cl } for _, in := range *ins { ast.Walk(cl, in) } if hasNoLinks(*ins) { return nil } result := make(ast.InlineSlice, 0, len(*ins)) for _, in := range *ins { switch n := in.(type) { case *ast.LinkNode: result = append(result, n.Inlines...) case *ast.FootnoteNode: // Do nothing default: result = append(result, n) } } *ins = result return nil } func hasNoLinks(ins ast.InlineSlice) bool { for _, in := range ins { switch in.(type) { case *ast.LinkNode, *ast.FootnoteNode: return false } } return true } |
Added internal/parser/draw.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package parser // draw provides a parser to create SVG from ASCII drawing. import ( "strconv" "t73f.de/r/sx" "t73f.de/r/webs/aasvg" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/ast" ) func init() { register(&Info{ Name: meta.ValueSyntaxDraw, AltNames: []string{}, IsASTParser: true, IsTextFormat: true, IsImageFormat: false, Parse: parseDraw, }) } const ( defaultFont = "" defaultScaleX = 10 defaultScaleY = 20 ) func parseDraw(inp *input.Input, m *meta.Meta, _ string) *sx.Pair { font := m.GetDefault("font", defaultFont) scaleX := m.GetNumber("x-scale", defaultScaleX) scaleY := m.GetNumber("y-scale", defaultScaleY) if scaleX < 1 || 1000000 < scaleX { scaleX = defaultScaleX } if scaleY < 1 || 1000000 < scaleY { scaleY = defaultScaleY } canvas, err := aasvg.NewCanvas(inp.Src[inp.Pos:]) if err != nil { return zsx.MakeBlock(zsx.MakeParaList(canvasErrMsg(err))) } svg := aasvg.CanvasToSVG(canvas, string(font), int(scaleX), int(scaleY)) if len(svg) == 0 { return zsx.MakeBlock(zsx.MakeParaList(noSVGErrMsg())) } return zsx.MakeBlock(zsx.MakeBLOB(nil, ParseDescription(m), meta.ValueSyntaxSVG, string(svg))) } // ParseDrawBlock parses the content of an eval verbatim node into an SVG image BLOB. func ParseDrawBlock(vn *ast.VerbatimNode) ast.BlockNode { font := defaultFont if val, found := vn.Attrs.Get("font"); found { font = val } scaleX := getScale(vn.Attrs, "x-scale", defaultScaleX) scaleY := getScale(vn.Attrs, "y-scale", defaultScaleY) canvas, err := aasvg.NewCanvas(vn.Content) if err != nil { return ast.CreateParaNode(ast.InlineSlice{&ast.TextNode{Text: "Error: " + err.Error()}}...) } if scaleX < 1 || 1000000 < scaleX { scaleX = defaultScaleX } if scaleY < 1 || 1000000 < scaleY { scaleY = defaultScaleY } svg := aasvg.CanvasToSVG(canvas, font, scaleX, scaleY) if len(svg) == 0 { return ast.CreateParaNode(ast.InlineSlice{&ast.TextNode{Text: "NO IMAGE"}}...) } return &ast.BLOBNode{ Description: nil, // TODO: look for attribute "summary" / "title" Syntax: meta.ValueSyntaxSVG, Blob: svg, } } func getScale(a zsx.Attributes, key string, defVal int) int { if val, found := a.Get(key); found { if n, err := strconv.Atoi(val); err == nil && 0 < n && n < 100000 { return n } } return defVal } func canvasErrMsg(err error) *sx.Pair { return sx.Cons(zsx.MakeText("Error: "+err.Error()), sx.Nil()) } func noSVGErrMsg() *sx.Pair { return sx.Cons(zsx.MakeText("NO IMAGE"), sx.Nil()) } |
Added internal/parser/draw_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package parser_test import ( "testing" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/parser" ) func FuzzParseDraw(f *testing.F) { f.Fuzz(func(t *testing.T, src []byte) { t.Parallel() inp := input.NewInput(src) parser.Parse(inp, nil, meta.ValueSyntaxDraw, config.NoHTML) }) } |
Added internal/parser/markdown.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package parser // markdown provides a parser for markdown. import ( "bytes" "fmt" "strconv" "strings" gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sz" "t73f.de/r/zsx" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/encoder" ) func init() { register(&Info{ Name: meta.ValueSyntaxMarkdown, AltNames: []string{meta.ValueSyntaxMD}, IsASTParser: true, IsTextFormat: true, IsImageFormat: false, Parse: parseMarkdown, }) } func parseMarkdown(inp *input.Input, _ *meta.Meta, _ string) *sx.Pair { source := []byte(inp.Src[inp.Pos:]) parser := gm.DefaultParser() node := parser.Parse(gmText.NewReader(source)) textEnc := encoder.Create(api.EncoderText, nil) p := mdP{source: source, docNode: node, textEnc: textEnc} return p.acceptBlockChildren(p.docNode) } type mdP struct { source []byte docNode gmAst.Node textEnc encoder.Encoder } func (p *mdP) acceptBlockChildren(docNode gmAst.Node) *sx.Pair { if docNode.Type() != gmAst.TypeDocument { panic(fmt.Sprintf("Expected document, but got node type %v", docNode.Type())) } var result sx.ListBuilder result.Add(zsx.SymBlock) for child := docNode.FirstChild(); child != nil; child = child.NextSibling() { if block := p.acceptBlock(child); block != nil { result.Add(block) } } return result.List() } func (p *mdP) acceptBlock(node gmAst.Node) *sx.Pair { if node.Type() != gmAst.TypeBlock { panic(fmt.Sprintf("Expected block node, but got node type %v", node.Type())) } switch n := node.(type) { case *gmAst.Paragraph: return p.acceptParagraph(n) case *gmAst.TextBlock: return p.acceptTextBlock(n) case *gmAst.Heading: return p.acceptHeading(n) case *gmAst.ThematicBreak: return p.acceptThematicBreak() case *gmAst.CodeBlock: return p.acceptCodeBlock(n) case *gmAst.FencedCodeBlock: return p.acceptFencedCodeBlock(n) case *gmAst.Blockquote: return p.acceptBlockquote(n) case *gmAst.List: return p.acceptList(n) case *gmAst.HTMLBlock: return p.acceptHTMLBlock(n) } panic(fmt.Sprintf("Unhandled block node of kind %v", node.Kind())) } func (p *mdP) acceptParagraph(node *gmAst.Paragraph) *sx.Pair { if is := p.acceptInlineChildren(node); is != nil { return zsx.MakeParaList(is) } return nil } func (p *mdP) acceptHeading(node *gmAst.Heading) *sx.Pair { return zsx.MakeHeading(node.Level, nil, p.acceptInlineChildren(node), "", "") } func (*mdP) acceptThematicBreak() *sx.Pair { return zsx.MakeThematic(nil /*TODO*/) } func (p *mdP) acceptCodeBlock(node *gmAst.CodeBlock) *sx.Pair { return zsx.MakeVerbatim(zsx.SymVerbatimCode, nil /*TODO*/, string(p.acceptRawText(node))) } func (p *mdP) acceptFencedCodeBlock(node *gmAst.FencedCodeBlock) *sx.Pair { var a sx.ListBuilder if language := node.Language(p.source); len(language) > 0 { a.Add(sx.Cons(sx.MakeString("class"), sx.MakeString("language-"+cleanText(language, true)))) } return zsx.MakeVerbatim(zsx.SymVerbatimCode, a.List(), string(p.acceptRawText(node))) } func (p *mdP) acceptRawText(node gmAst.Node) []byte { lines := node.Lines() result := make([]byte, 0, 512) for i := range lines.Len() { s := lines.At(i) line := s.Value(p.source) if l := len(line); l > 0 { if l > 1 && line[l-2] == '\r' && line[l-1] == '\n' { line = line[0 : l-2] } else if line[l-1] == '\n' || line[l-1] == '\r' { line = line[0 : l-1] } } if i > 0 { result = append(result, '\n') } result = append(result, line...) } return result } func (p *mdP) acceptBlockquote(node *gmAst.Blockquote) *sx.Pair { return zsx.MakeList(zsx.SymListQuote, nil, p.acceptItemSlice(node)) } func (p *mdP) acceptList(node *gmAst.List) *sx.Pair { kind := zsx.SymListUnordered var a sx.ListBuilder if node.IsOrdered() { kind = zsx.SymListOrdered if node.Start != 1 { a.Add(sx.Cons(sx.MakeString("start"), sx.MakeString(strconv.Itoa(node.Start)))) } } var items sx.ListBuilder for child := node.FirstChild(); child != nil; child = child.NextSibling() { item, ok := child.(*gmAst.ListItem) if !ok { panic(fmt.Sprintf("Expected list item node, but got %v", child.Kind())) } items.Add(p.acceptItemSlice(item)) } return zsx.MakeList(kind, a.List(), items.List()) } func (p *mdP) acceptItemSlice(node gmAst.Node) *sx.Pair { var result sx.ListBuilder for elem := node.FirstChild(); elem != nil; elem = elem.NextSibling() { if item := p.acceptBlock(elem); item != nil { result.Add(item) } } return result.List() } func (p *mdP) acceptTextBlock(node *gmAst.TextBlock) *sx.Pair { if is := p.acceptInlineChildren(node); is != nil { return zsx.MakeParaList(is) } return nil } func (p *mdP) acceptHTMLBlock(node *gmAst.HTMLBlock) *sx.Pair { content := p.acceptRawText(node) if node.HasClosure() { closure := node.ClosureLine.Value(p.source) if l := len(closure); l > 1 && closure[l-1] == '\n' { closure = closure[:l-1] } if len(content) > 1 { content = append(content, '\n') } content = append(content, closure...) } return zsx.MakeVerbatim(zsx.SymVerbatimHTML, nil, string(content)) } func (p *mdP) acceptInlineChildren(node gmAst.Node) *sx.Pair { var result sx.ListBuilder for child := node.FirstChild(); child != nil; child = child.NextSibling() { n1, n2 := p.acceptInline(child) if n1 != nil { result.Add(n1) } if n2 != nil { result.Add(n2) } } return result.List() } func (p *mdP) acceptInline(node gmAst.Node) (*sx.Pair, *sx.Pair) { if node.Type() != gmAst.TypeInline { panic(fmt.Sprintf("Expected inline node, but got %v", node.Type())) } switch n := node.(type) { case *gmAst.Text: return p.acceptText(n) case *gmAst.CodeSpan: return p.acceptCodeSpan(n) case *gmAst.Emphasis: return p.acceptEmphasis(n) case *gmAst.Link: return p.acceptLink(n) case *gmAst.Image: return p.acceptImage(n) case *gmAst.AutoLink: return p.acceptAutoLink(n) case *gmAst.RawHTML: return p.acceptRawHTML(n) } panic(fmt.Sprintf("Unhandled inline node %v", node.Kind())) } func (p *mdP) acceptText(node *gmAst.Text) (*sx.Pair, *sx.Pair) { segment := node.Segment text := segment.Value(p.source) if text == nil { return nil, nil } if node.IsRaw() { return zsx.MakeText(string(text)), nil } in := zsx.MakeText(cleanText(text, true)) if node.HardLineBreak() { return in, zsx.MakeHard() } if node.SoftLineBreak() { return in, zsx.MakeSoft() } return in, nil } var ignoreAfterBS = map[byte]struct{}{ '!': {}, '"': {}, '#': {}, '$': {}, '%': {}, '&': {}, '\'': {}, '(': {}, ')': {}, '*': {}, '+': {}, ',': {}, '-': {}, '.': {}, '/': {}, ':': {}, ';': {}, '<': {}, '=': {}, '>': {}, '?': {}, '@': {}, '[': {}, '\\': {}, ']': {}, '^': {}, '_': {}, '`': {}, '{': {}, '|': {}, '}': {}, '~': {}, } // cleanText removes backslashes from TextNodes and expands entities func cleanText(text []byte, cleanBS bool) string { lastPos := 0 var sb strings.Builder for pos, ch := range text { if pos < lastPos { continue } if ch == '&' { inp := input.NewInput([]byte(text[pos:])) if s, ok := zsx.ScanEntity(inp); ok { sb.Write(text[lastPos:pos]) sb.WriteString(s) lastPos = pos + inp.Pos } continue } if cleanBS && ch == '\\' && pos < len(text)-1 { if _, found := ignoreAfterBS[text[pos+1]]; found { sb.Write(text[lastPos:pos]) sb.WriteByte(text[pos+1]) lastPos = pos + 2 } } } if lastPos < len(text) { sb.Write(text[lastPos:]) } return sb.String() } func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) (*sx.Pair, *sx.Pair) { var segBuf strings.Builder for c := node.FirstChild(); c != nil; c = c.NextSibling() { segment := c.(*gmAst.Text).Segment segBuf.Write(segment.Value(p.source)) } content := segBuf.String() // Clean code span if len(content) > 0 { lastPos := 0 var buf strings.Builder for pos, ch := range content { if ch == '\n' { buf.WriteString(content[lastPos:pos]) if pos < len(content)-1 { buf.WriteByte(' ') } lastPos = pos + 1 } } buf.WriteString(content[lastPos:]) content = buf.String() } return zsx.MakeLiteral(zsx.SymLiteralCode, nil /* TODO */, content), nil } func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) (*sx.Pair, *sx.Pair) { sym := zsx.SymFormatEmph if node.Level == 2 { sym = zsx.SymFormatStrong } return zsx.MakeFormat(sym, nil /* TODO */, p.acceptInlineChildren(node)), nil } func (p *mdP) acceptLink(node *gmAst.Link) (*sx.Pair, *sx.Pair) { ref := sz.ScanReference(cleanText(node.Destination, true)) var a sx.ListBuilder if title := node.Title; len(title) > 0 { a.Add(sx.Cons(sx.MakeString("title"), sx.MakeString(cleanText(title, true)))) } return zsx.MakeLink(a.List(), ref, p.acceptInlineChildren(node)), nil } func (p *mdP) acceptImage(node *gmAst.Image) (*sx.Pair, *sx.Pair) { ref := sz.ScanReference(cleanText(node.Destination, true)) var a sx.ListBuilder if title := node.Title; len(title) > 0 { a.Add(sx.Cons(sx.MakeString("title"), sx.MakeString(cleanText(title, true)))) } return zsx.MakeEmbed(a.List(), ref, "", p.acceptInlineChildren(node)), nil } func (p *mdP) acceptAutoLink(node *gmAst.AutoLink) (*sx.Pair, *sx.Pair) { u := node.URL(p.source) if node.AutoLinkType == gmAst.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(u), []byte("mailto:")) { u = append([]byte("mailto:"), u...) } return zsx.MakeLink(nil /* TODO */, sz.ScanReference(cleanText(u, false)), nil), nil } func (p *mdP) acceptRawHTML(node *gmAst.RawHTML) (*sx.Pair, *sx.Pair) { segs := make([][]byte, 0, node.Segments.Len()) for i := range node.Segments.Len() { segment := node.Segments.At(i) segs = append(segs, segment.Value(p.source)) } return zsx.MakeLiteral( zsx.SymLiteralCode, sx.Cons(sx.Cons(sx.MakeString(""), sx.MakeString("html")), sx.Nil()), string(bytes.Join(segs, nil)), ), nil } |
Added internal/parser/none.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package parser import ( "t73f.de/r/sx" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" ) // none provides a none-parser, e.g. for zettel with just metadata. func init() { register(&Info{ Name: meta.ValueSyntaxNone, AltNames: []string{}, IsASTParser: false, IsTextFormat: false, IsImageFormat: false, Parse: func(*input.Input, *meta.Meta, string) *sx.Pair { return nil }, }) } |
Added internal/parser/parser.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package parser provides a generic interface to a range of different parsers. package parser import ( "context" "fmt" "log" "t73f.de/r/sx" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/ast/sztrans" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/zettel" ) // Info describes a single parser. // // Before Parse() is called, ensure the input stream to be valid. This can be // achieved on calling inp.Next() after the input stream was created. type Info struct { Name string AltNames []string IsASTParser bool IsTextFormat bool IsImageFormat bool Parse func(*input.Input, *meta.Meta, string) *sx.Pair } var registry = map[string]*Info{} // register the parser (info) for later retrieval. func register(pi *Info) { if _, ok := registry[pi.Name]; ok { panic(fmt.Sprintf("Parser %q already registered", pi.Name)) } registry[pi.Name] = pi for _, alt := range pi.AltNames { if _, ok := registry[alt]; ok { panic(fmt.Sprintf("Parser %q already registered", alt)) } registry[alt] = pi } } // GetSyntaxes returns a list of syntaxes implemented by all registered parsers. func GetSyntaxes() []string { result := make([]string, 0, len(registry)) for syntax := range registry { result = append(result, syntax) } return result } // Get the parser (info) by name. If name not found, use a default parser. func Get(name string) *Info { if pi := registry[name]; pi != nil { return pi } if pi := registry["plain"]; pi != nil { return pi } panic(fmt.Sprintf("No parser for %q found", name)) } // IsASTParser returns whether the given syntax parses text into an AST or not. func IsASTParser(syntax string) bool { pi, ok := registry[syntax] if !ok { return false } return pi.IsASTParser } // IsImageFormat returns whether the given syntax is known to be an image format. func IsImageFormat(syntax string) bool { pi, ok := registry[syntax] if !ok { return false } return pi.IsImageFormat } // Parse parses some input and returns a slice of block nodes. func Parse(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice { if obj := Get(syntax).Parse(inp, m, syntax); obj != nil { bs, err := sztrans.GetBlockSlice(obj) if err == nil { Clean(&bs, hi.AllowHTML(syntax)) return bs } log.Printf("sztrans error: %v, for %v\n", err, obj) } return nil } // ParseDescriptionAST returns a suitable description stored in the metadata as an inline slice. // This is done for an image in most cases. func ParseDescriptionAST(m *meta.Meta) ast.InlineSlice { if m == nil { return nil } if summary, found := m.Get(meta.KeySummary); found { return ast.ParseSpacedText(string(summary)) } if title, found := m.Get(meta.KeyTitle); found { return ast.ParseSpacedText(string(title)) } return ast.InlineSlice{&ast.TextNode{Text: "Zettel without title/summary: " + m.Zid.String()}} } // ParseDescription returns a suitable description stored in the metadata as an inline list. // This is done for an image in most cases. func ParseDescription(m *meta.Meta) *sx.Pair { if m == nil { return nil } if summary, found := m.Get(meta.KeySummary); found { return sx.Cons(zsx.MakeText(ast.NormalizedSpacedText(string(summary))), sx.Nil()) } if title, found := m.Get(meta.KeyTitle); found { return sx.Cons(zsx.MakeText(ast.NormalizedSpacedText(string(title))), sx.Nil()) } return sx.Cons(zsx.MakeText("Zettel without title/summary: "+m.Zid.String()), sx.Nil()) } // ParseZettel parses the zettel based on the syntax. func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode { m := zettel.Meta inhMeta := m if rtConfig != nil { inhMeta = rtConfig.AddDefaultValues(ctx, inhMeta) } if syntax == "" { syntax = string(inhMeta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)) } parseMeta := inhMeta if syntax == meta.ValueSyntaxNone { parseMeta = m } hi := config.NoHTML if rtConfig != nil { hi = rtConfig.GetHTMLInsecurity() } bs := Parse(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, hi) return &ast.ZettelNode{ Meta: m, Content: zettel.Content, Zid: m.Zid, InhMeta: inhMeta, BlocksAST: bs, Syntax: syntax, } } |
Added internal/parser/parser_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package parser_test import ( "testing" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/parser" ) func TestParserType(t *testing.T) { syntaxSet := set.New(parser.GetSyntaxes()...) testCases := []struct { syntax string ast bool image bool }{ {meta.ValueSyntaxHTML, false, false}, {meta.ValueSyntaxCSS, false, false}, {meta.ValueSyntaxDraw, true, false}, {meta.ValueSyntaxGif, false, true}, {meta.ValueSyntaxJPEG, false, true}, {meta.ValueSyntaxJPG, false, true}, {meta.ValueSyntaxMarkdown, true, false}, {meta.ValueSyntaxMD, true, false}, {meta.ValueSyntaxNone, false, false}, {meta.ValueSyntaxPlain, false, false}, {meta.ValueSyntaxPNG, false, true}, {meta.ValueSyntaxSVG, false, true}, {meta.ValueSyntaxSxn, false, false}, {meta.ValueSyntaxText, false, false}, {meta.ValueSyntaxTxt, false, false}, {meta.ValueSyntaxWebp, false, true}, {meta.ValueSyntaxZmk, true, false}, } for _, tc := range testCases { syntaxSet.Remove(tc.syntax) if got := parser.IsASTParser(tc.syntax); got != tc.ast { t.Errorf("Syntax %q is AST: %v, but got %v", tc.syntax, tc.ast, got) } if got := parser.IsImageFormat(tc.syntax); got != tc.image { t.Errorf("Syntax %q is image: %v, but got %v", tc.syntax, tc.image, got) } } for syntax := range syntaxSet.Values() { t.Errorf("Forgot to test syntax %q", syntax) } } |
Added internal/parser/plain.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package parser // plain provides a parser for plain text data. import ( "bytes" "t73f.de/r/sx" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx" "t73f.de/r/zsx/input" ) func init() { register(&Info{ Name: meta.ValueSyntaxTxt, AltNames: []string{meta.ValueSyntaxPlain, meta.ValueSyntaxText}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, Parse: parsePlain, }) register(&Info{ Name: meta.ValueSyntaxHTML, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, Parse: parsePlainHTML, }) register(&Info{ Name: meta.ValueSyntaxCSS, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, Parse: parsePlain, }) register(&Info{ Name: meta.ValueSyntaxSVG, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: true, Parse: parsePlainSVG, }) register(&Info{ Name: meta.ValueSyntaxSxn, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, Parse: parsePlainSxn, }) } func parsePlain(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair { return doParsePlain(inp, syntax, zsx.SymVerbatimCode) } func parsePlainHTML(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair { return doParsePlain(inp, syntax, zsx.SymVerbatimHTML) } func doParsePlain(inp *input.Input, syntax string, kind *sx.Symbol) *sx.Pair { return zsx.MakeBlock(zsx.MakeVerbatim( kind, sx.Cons(sx.Cons(sx.MakeString(""), sx.MakeString(syntax)), sx.Nil()), string(inp.ScanLineContent()), )) } func parsePlainSVG(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair { is := parseSVGInlines(inp, syntax) if is == nil { return nil } return zsx.MakeBlock(zsx.MakeParaList(is)) } func parseSVGInlines(inp *input.Input, syntax string) *sx.Pair { svgSrc := scanSVG(inp) if svgSrc == "" { return nil } return sx.Cons(zsx.MakeEmbedBLOB(nil, syntax, svgSrc, nil), sx.Nil()) } func scanSVG(inp *input.Input) string { inp.SkipSpace() pos := inp.Pos if !inp.Accept("<svg") { return "" } ch := inp.Ch if input.IsSpace(ch) || input.IsEOLEOS(ch) || ch == '>' { // TODO: check proper end </svg> return string(inp.Src[pos:]) } return "" } func parsePlainSxn(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair { rd := sxreader.MakeReader(bytes.NewReader(inp.Src)) _, err := rd.ReadAll() var blocks sx.ListBuilder blocks.Add(zsx.MakeVerbatim( zsx.SymVerbatimCode, sx.Cons(sx.Cons(sx.MakeString(""), sx.MakeString(syntax)), sx.Nil()), string(inp.ScanLineContent()), )) if err != nil { blocks.Add(zsx.MakeParaList(sx.Cons(zsx.MakeText(err.Error()), sx.Nil()))) } return zsx.MakeBlockList(blocks.List()) } |
Added internal/parser/plain_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | //----------------------------------------------------------------------------- // Copyright (c) 2024-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2024-present Detlef Stern //----------------------------------------------------------------------------- package parser_test import ( "testing" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/parser" ) func TestParseSVG(t *testing.T) { testCases := []struct { name string src string exp string }{ {"common", " <svg bla", "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg bla\")))"}, {"inkscape", "<svg\nbla", "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg\\nbla\")))"}, {"selfmade", "<svg>", "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg>\")))"}, {"error", "<svgbla", "(BLOCK)"}, {"error-", "<svg-bla", "(BLOCK)"}, {"error#", "<svg2bla", "(BLOCK)"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { inp := input.NewInput([]byte(tc.src)) bs := parser.Parse(inp, nil, meta.ValueSyntaxSVG, config.NoHTML) trans := encoder.NewSzTransformer() lst := trans.GetSz(&bs) if got := lst.String(); tc.exp != got { t.Errorf("\nexp: %q\ngot: %q", tc.exp, got) } }) } } |
Added internal/parser/zettelmark.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package parser // zettelmark provides a parser for zettelmarkup. import ( "t73f.de/r/sx" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sz/zmk" "t73f.de/r/zsx/input" ) func init() { register(&Info{ Name: meta.ValueSyntaxZmk, AltNames: nil, IsASTParser: true, IsTextFormat: true, IsImageFormat: false, Parse: func(inp *input.Input, _ *meta.Meta, _ string) *sx.Pair { var parser zmk.Parser parser.Initialize(inp) return parser.Parse() }, }) } |
Added internal/parser/zettelmark_fuzz_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package parser_test import ( "testing" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/parser" ) func FuzzParseZmk(f *testing.F) { f.Fuzz(func(t *testing.T, src []byte) { t.Parallel() inp := input.NewInput(src) parser.Parse(inp, nil, meta.ValueSyntaxZmk, config.NoHTML) }) } |
Added internal/parser/zettelmark_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package parser_test import ( "fmt" "strings" "testing" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/parser" ) type TestCase struct{ source, want string } type TestCases []TestCase func replace(s string, tcs TestCases) TestCases { var testCases TestCases for _, tc := range tcs { source := strings.ReplaceAll(tc.source, "$", s) want := strings.ReplaceAll(tc.want, "$", s) testCases = append(testCases, TestCase{source, want}) } return testCases } func checkTcs(t *testing.T, tcs TestCases) { t.Helper() for tcn, tc := range tcs { t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) { st.Helper() inp := input.NewInput([]byte(tc.source)) bns := parser.Parse(inp, nil, meta.ValueSyntaxZmk, config.NoHTML) var tv TestVisitor ast.Walk(&tv, &bns) got := tv.String() if tc.want != got { st.Errorf("\nwant=%q\n got=%q", tc.want, got) } }) } } func TestEOL(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"", ""}, {"\n", ""}, {"\r", ""}, {"\r\n", ""}, {"\n\n", ""}, }) } func TestText(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"abcd", "(PARA abcd)"}, {"ab cd", "(PARA ab cd)"}, {"abcd ", "(PARA abcd)"}, {" abcd", "(PARA abcd)"}, {"\\", "(PARA \\)"}, {"\\\n", ""}, {"\\\ndef", "(PARA HB def)"}, {"\\\r", ""}, {"\\\rdef", "(PARA HB def)"}, {"\\\r\n", ""}, {"\\\r\ndef", "(PARA HB def)"}, {"\\a", "(PARA a)"}, {"\\aa", "(PARA aa)"}, {"a\\a", "(PARA aa)"}, {"\\+", "(PARA +)"}, {"\\ ", "(PARA \u00a0)"}, {"http://a, http://b", "(PARA http://a, http://b)"}, }) } func TestSpace(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {" ", ""}, {"\t", ""}, {" ", ""}, }) } func TestSoftBreak(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"x\ny", "(PARA x SB y)"}, {"z\n", "(PARA z)"}, {" \n ", ""}, {" \n", ""}, }) } func TestHardBreak(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"x \ny", "(PARA x HB y)"}, {"z \n", "(PARA z)"}, {" \n ", ""}, {" \n", ""}, }) } func TestLink(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"[", "(PARA [)"}, {"[[", "(PARA [[)"}, {"[[|", "(PARA [[|)"}, {"[[]", "(PARA [[])"}, {"[[|]", "(PARA [[|])"}, {"[[]]", "(PARA [[]])"}, {"[[|]]", "(PARA [[|]])"}, {"[[ ]]", "(PARA [[ ]])"}, {"[[\n]]", "(PARA [[ SB ]])"}, {"[[ a]]", "(PARA (LINK a))"}, {"[[a ]]", "(PARA [[a ]])"}, {"[[a\n]]", "(PARA [[a SB ]])"}, {"[[a]]", "(PARA (LINK a))"}, {"[[12345678901234]]", "(PARA (LINK 12345678901234))"}, {"[[a]", "(PARA [[a])"}, {"[[|a]]", "(PARA [[|a]])"}, {"[[b|]]", "(PARA [[b|]])"}, {"[[b|a]]", "(PARA (LINK a b))"}, {"[[b| a]]", "(PARA (LINK a b))"}, {"[[b%c|a]]", "(PARA (LINK a b%c))"}, {"[[b%%c|a]]", "(PARA [[b {% c|a]]})"}, {"[[b|a]", "(PARA [[b|a])"}, {"[[b\nc|a]]", "(PARA (LINK a b SB c))"}, {"[[b c|a#n]]", "(PARA (LINK a#n b c))"}, {"[[a]]go", "(PARA (LINK a) go)"}, {"[[b|a]]{go}", "(PARA (LINK a b)[ATTR go])"}, {"[[[[a]]|b]]", "(PARA [[ (LINK a) |b]])"}, {"[[a[b]c|d]]", "(PARA (LINK d a[b]c))"}, {"[[[b]c|d]]", "(PARA [ (LINK d b]c))"}, {"[[a[]c|d]]", "(PARA (LINK d a[]c))"}, {"[[a[b]|d]]", "(PARA (LINK d a[b]))"}, {"[[\\|]]", "(PARA (LINK %5C%7C))"}, {"[[\\||a]]", "(PARA (LINK a |))"}, {"[[b\\||a]]", "(PARA (LINK a b|))"}, {"[[b\\|c|a]]", "(PARA (LINK a b|c))"}, {"[[\\]]]", "(PARA (LINK %5C%5D))"}, {"[[\\]|a]]", "(PARA (LINK a ]))"}, {"[[b\\]|a]]", "(PARA (LINK a b]))"}, {"[[\\]\\||a]]", "(PARA (LINK a ]|))"}, {"[[http://a]]", "(PARA (LINK http://a))"}, {"[[http://a|http://a]]", "(PARA (LINK http://a http://a))"}, {"[[[[a]]]]", "(PARA [[ (LINK a) ]])"}, {"[[query:title]]", "(PARA (LINK query:title))"}, {"[[query:title syntax]]", "(PARA (LINK query:title syntax))"}, {"[[query:title | action]]", "(PARA (LINK query:title | action))"}, {"[[Text|query:title]]", "(PARA (LINK query:title Text))"}, {"[[Text|query:title syntax]]", "(PARA (LINK query:title syntax Text))"}, {"[[Text|query:title | action]]", "(PARA (LINK query:title | action Text))"}, }) } func TestCite(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"[@", "(PARA [@)"}, {"[@]", "(PARA [@])"}, {"[@a]", "(PARA (CITE a))"}, {"[@ a]", "(PARA [@ a])"}, {"[@a ]", "(PARA (CITE a))"}, {"[@a\n]", "(PARA (CITE a))"}, {"[@a\nx]", "(PARA (CITE a SB x))"}, {"[@a\n\n]", "(PARA [@a)(PARA ])"}, {"[@a,\n]", "(PARA (CITE a))"}, {"[@a,n]", "(PARA (CITE a n))"}, {"[@a| n]", "(PARA (CITE a n))"}, {"[@a|n ]", "(PARA (CITE a n))"}, {"[@a,[@b]]", "(PARA (CITE a (CITE b)))"}, {"[@a]{color=green}", "(PARA (CITE a)[ATTR color=green])"}, }) } func TestFootnote(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"[^", "(PARA [^)"}, {"[^]", "(PARA (FN))"}, {"[^abc]", "(PARA (FN abc))"}, {"[^abc ]", "(PARA (FN abc))"}, {"[^abc\ndef]", "(PARA (FN abc SB def))"}, {"[^abc\n\ndef]", "(PARA [^abc)(PARA def])"}, {"[^abc[^def]]", "(PARA (FN abc (FN def)))"}, {"[^abc]{-}", "(PARA (FN abc)[ATTR -])"}, }) } func TestEmbed(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"{", "(PARA {)"}, {"{{", "(PARA {{)"}, {"{{|", "(PARA {{|)"}, {"{{}", "(PARA {{})"}, {"{{|}", "(PARA {{|})"}, {"{{}}", "(PARA {{}})"}, {"{{|}}", "(PARA {{|}})"}, {"{{ }}", "(PARA {{ }})"}, {"{{\n}}", "(PARA {{ SB }})"}, {"{{a }}", "(PARA {{a }})"}, {"{{a\n}}", "(PARA {{a SB }})"}, {"{{a}}", "(PARA (EMBED a))"}, {"{{12345678901234}}", "(PARA (EMBED 12345678901234))"}, {"{{ a}}", "(PARA (EMBED a))"}, {"{{a}", "(PARA {{a})"}, {"{{|a}}", "(PARA {{|a}})"}, {"{{b|}}", "(PARA {{b|}})"}, {"{{b|a}}", "(PARA (EMBED a b))"}, {"{{b| a}}", "(PARA (EMBED a b))"}, {"{{b|a}", "(PARA {{b|a})"}, {"{{b\nc|a}}", "(PARA (EMBED a b SB c))"}, {"{{b c|a#n}}", "(PARA (EMBED a#n b c))"}, {"{{a}}{go}", "(PARA (EMBED a)[ATTR go])"}, {"{{{{a}}|b}}", "(PARA {{ (EMBED a) |b}})"}, {"{{\\|}}", "(PARA (EMBED %5C%7C))"}, {"{{\\||a}}", "(PARA (EMBED a |))"}, {"{{b\\||a}}", "(PARA (EMBED a b|))"}, {"{{b\\|c|a}}", "(PARA (EMBED a b|c))"}, {"{{\\}}}", "(PARA (EMBED %5C%7D))"}, {"{{\\}|a}}", "(PARA (EMBED a }))"}, {"{{b\\}|a}}", "(PARA (EMBED a b}))"}, {"{{\\}\\||a}}", "(PARA (EMBED a }|))"}, {"{{http://a}}", "(PARA (EMBED http://a))"}, {"{{http://a|http://a}}", "(PARA (EMBED http://a http://a))"}, {"{{{{a}}}}", "(PARA {{ (EMBED a) }})"}, }) } func TestMark(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"[!", "(PARA [!)"}, {"[!\n", "(PARA [!)"}, {"[!]", "(PARA (MARK #*))"}, {"[!][!]", "(PARA (MARK #*) (MARK #*-1))"}, {"[! ]", "(PARA [! ])"}, {"[!a]", "(PARA (MARK \"a\" #a))"}, {"[!a][!a]", "(PARA (MARK \"a\" #a) (MARK \"a\" #a-1))"}, {"[!a ]", "(PARA [!a ])"}, {"[!a_]", "(PARA (MARK \"a_\" #a))"}, {"[!a_][!a]", "(PARA (MARK \"a_\" #a) (MARK \"a\" #a-1))"}, {"[!a-b]", "(PARA (MARK \"a-b\" #a-b))"}, {"[!a|b]", "(PARA (MARK \"a\" #a b))"}, {"[!a|]", "(PARA (MARK \"a\" #a))"}, {"[!|b]", "(PARA (MARK #* b))"}, {"[!|b c]", "(PARA (MARK #* b c))"}, }) } func TestComment(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"%", "(PARA %)"}, {"%%", "(PARA {%})"}, {"%\n", "(PARA %)"}, {"%%\n", "(PARA {%})"}, {"%%a", "(PARA {% a})"}, {"%%%a", "(PARA {% a})"}, {"%% a", "(PARA {% a})"}, {"%%% a", "(PARA {% a})"}, {"%% % a", "(PARA {% % a})"}, {"%%a", "(PARA {% a})"}, {"a%%b", "(PARA a {% b})"}, {"a %%b", "(PARA a {% b})" /*"(PARA a {% b})"*/}, {" %%b", "(PARA {% b})"}, {"%%b ", "(PARA {% b })"}, {"100%", "(PARA 100%)"}, }) } func TestFormat(t *testing.T) { t.Parallel() // Not for Insert / '>', because collision with quoted list for _, ch := range []string{"_", "*", "~", "^", ",", "\"", "#", ":"} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, })) } for _, ch := range []string{"_", "*", ">", "~", "^", ",", "\"", "#", ":"} { checkTcs(t, replace(ch, TestCases{ {"$$a$$", "(PARA {$ a})"}, {"$$a$$$", "(PARA {$ a} $)"}, {"$$$a$$", "(PARA {$ $a})"}, {"$$$a$$$", "(PARA {$ $a} $)"}, {"$\\$", "(PARA $$)"}, {"$\\$$", "(PARA $$$)"}, {"$$\\$", "(PARA $$$)"}, {"$$a\\$$", "(PARA $$a$$)"}, {"$$a$\\$", "(PARA $$a$$)"}, {"$$a\\$$$", "(PARA {$ a$})"}, {"$$a\na$$", "(PARA {$ a SB a})"}, {"$$a\n\na$$", "(PARA $$a)(PARA a$$)"}, {"$$a$${go}", "(PARA {$ a}[ATTR go])"}, })) } checkTcs(t, TestCases{ {"__****__", "(PARA {_ {*}})"}, {"__**a**__", "(PARA {_ {* a}})"}, {"__**__**", "(PARA __ {* __})"}, }) } func TestLiteral(t *testing.T) { t.Parallel() for _, ch := range []string{"`", "'", "="} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, {"$$a$$", "(PARA {$ a})"}, {"$$a$$$", "(PARA {$ a} $)"}, {"$$$a$$", "(PARA {$ $a})"}, {"$$$a$$$", "(PARA {$ $a} $)"}, {"$\\$", "(PARA $$)"}, {"$\\$$", "(PARA $$$)"}, {"$$\\$", "(PARA $$$)"}, {"$$a\\$$", "(PARA $$a$$)"}, {"$$a$\\$", "(PARA $$a$$)"}, {"$$a\\$$$", "(PARA {$ a$})"}, {"$$a$${go}", "(PARA {$ a}[ATTR go])"}, })) } checkTcs(t, TestCases{ {"``<script `` abc", "(PARA {` <script } abc)"}, {"''````''", "(PARA {' ````})"}, {"''``a``''", "(PARA {' ``a``})"}, {"''``''``", "(PARA {' ``} ``)"}, {"''\\'''", "(PARA {' '})"}, }) } func TestLiteralMath(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, {"$$a$$", "(PARA {$ a})"}, {"$$a$$$", "(PARA {$ a} $)"}, {"$$$a$$", "(PARA {$ $a})"}, {"$$$a$$$", "(PARA {$ $a} $)"}, {`$\$`, "(PARA $$)"}, {`$\$$`, "(PARA $$$)"}, {`$$\$`, "(PARA $$$)"}, {`$$a\$$`, `(PARA {$ a\})`}, {`$$a$\$`, "(PARA $$a$$)"}, {`$$a\$$$`, `(PARA {$ a\} $)`}, {"$$a$${go}", "(PARA {$ a}[ATTR go])"}, }) } func TestMixFormatCode(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"__abc__\n**def**", "(PARA {_ abc} SB {* def})"}, {"''abc''\n==def==", "(PARA {' abc} SB {= def})"}, {"__abc__\n==def==", "(PARA {_ abc} SB {= def})"}, {"__abc__\n``def``", "(PARA {_ abc} SB {` def})"}, {"\"\"ghi\"\"\n::abc::\n``def``\n", "(PARA {\" ghi} SB {: abc} SB {` def})"}, }) } func TestNDash(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"--", "(PARA \u2013)"}, {"a--b", "(PARA a\u2013b)"}, }) } func TestEntity(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"&", "(PARA &)"}, {"&;", "(PARA &;)"}, {"&#;", "(PARA &#;)"}, {"a;", "(PARA a;)"}, {"&#x;", "(PARA &#x;)"}, {"�z;", "(PARA �z;)"}, {"&1;", "(PARA &1;)"}, {"	", "(PARA 	)"}, // No numeric entities below space are not allowed. {"", "(PARA )"}, // Good cases {"<", "(PARA <)"}, {"0", "(PARA 0)"}, {"J", "(PARA J)"}, {"J", "(PARA J)"}, {"…", "(PARA \u2026)"}, {" ", "(PARA \u00a0)"}, {"E: &,?;c.", "(PARA E: &,?;c.)"}, }) } func TestVerbatimZettel(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"@@@\n@@@", "(ZETTEL)"}, {"@@@\nabc\n@@@", "(ZETTEL\nabc)"}, {"@@@@def\nabc\n@@@@", "(ZETTEL\nabc)[ATTR =def]"}, }) } func TestVerbatimCode(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"```\n```", "(PROG)"}, {"```\nabc\n```", "(PROG\nabc)"}, {"```\nabc\n````", "(PROG\nabc)"}, {"````\nabc\n````", "(PROG\nabc)"}, {"````\nabc\n```\n````", "(PROG\nabc\n```)"}, {"````go\nabc\n````", "(PROG\nabc)[ATTR =go]"}, }) } func TestVerbatimEval(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"~~~\n~~~", "(EVAL)"}, {"~~~\nabc\n~~~", "(EVAL\nabc)"}, {"~~~\nabc\n~~~~", "(EVAL\nabc)"}, {"~~~~\nabc\n~~~~", "(EVAL\nabc)"}, {"~~~~\nabc\n~~~\n~~~~", "(EVAL\nabc\n~~~)"}, {"~~~~go\nabc\n~~~~", "(EVAL\nabc)[ATTR =go]"}, }) } func TestVerbatimMath(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"$$$\n$$$", "(MATH)"}, {"$$$\nabc\n$$$", "(MATH\nabc)"}, {"$$$\nabc\n$$$$", "(MATH\nabc)"}, {"$$$$\nabc\n$$$$", "(MATH\nabc)"}, {"$$$$\nabc\n$$$\n$$$$", "(MATH\nabc\n$$$)"}, {"$$$$go\nabc\n$$$$", "(MATH\nabc)[ATTR =go]"}, }) } func TestVerbatimComment(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"%%%\n%%%", "(COMMENT)"}, {"%%%\nabc\n%%%", "(COMMENT\nabc)"}, {"%%%%go\nabc\n%%%%", "(COMMENT\nabc)[ATTR =go]"}, }) } func TestPara(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"a\n\nb", "(PARA a)(PARA b)"}, {"a\n \nb", "(PARA a)(PARA b)"}, }) } func TestSpanRegion(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {":::\n:::", "(SPAN)"}, {":::\nabc\n:::", "(SPAN (PARA abc))"}, {":::\nabc\n::::", "(SPAN (PARA abc))"}, {"::::\nabc\n::::", "(SPAN (PARA abc))"}, {"::::\nabc\n:::\ndef\n:::\n::::", "(SPAN (PARA abc)(SPAN (PARA def)))"}, {":::{go}\na\n:::", "(SPAN (PARA a))[ATTR go]"}, {":::\nabc\n::: def ", "(SPAN (PARA abc) (LINE def))"}, }) } func TestQuoteRegion(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"<<<\n<<<", "(QUOTE)"}, {"<<<\nabc\n<<<", "(QUOTE (PARA abc))"}, {"<<<\nabc\n<<<<", "(QUOTE (PARA abc))"}, {"<<<<\nabc\n<<<<", "(QUOTE (PARA abc))"}, {"<<<<\nabc\n<<<\ndef\n<<<\n<<<<", "(QUOTE (PARA abc)(QUOTE (PARA def)))"}, {"<<<go\na\n<<<", "(QUOTE (PARA a))[ATTR =go]"}, {"<<<\nabc\n<<< def ", "(QUOTE (PARA abc) (LINE def))"}, }) } func TestVerseRegion(t *testing.T) { t.Parallel() checkTcs(t, replace("\"", TestCases{ // {"$$$\n$$$", "(VERSE)"}, {"$$$\nabc\n$$$", "(VERSE (PARA abc))"}, {"$$$\nabc\n$$$$", "(VERSE (PARA abc))"}, {"$$$$\nabc\n$$$$", "(VERSE (PARA abc))"}, {"$$$\nabc\ndef\n$$$", "(VERSE (PARA abc HB def))"}, {"$$$$\nabc\n$$$\ndef\n$$$\n$$$$", "(VERSE (PARA abc)(VERSE (PARA def)))"}, {"$$$go\na\n$$$", "(VERSE (PARA a))[ATTR =go]"}, {"$$$\nabc\n$$$ def ", "(VERSE (PARA abc) (LINE def))"}, })) } func TestHeading(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"=h", "(PARA =h)"}, {"= h", "(PARA = h)"}, {"==h", "(PARA ==h)"}, {"== h", "(PARA == h)"}, {"===h", "(PARA ===h)"}, {"=== h", "(H1 h #h)"}, {"=== h", "(H1 h #h)"}, {"==== h", "(H2 h #h)"}, {"===== h", "(H3 h #h)"}, {"====== h", "(H4 h #h)"}, {"======= h", "(H5 h #h)"}, {"======== h", "(H5 h #h)"}, {"=", "(PARA =)"}, {"=== h=__=a__", "(H1 h= {_ =a} #h-a)"}, {"=\n", "(PARA =)"}, {"a=", "(PARA a=)"}, {" =", "(PARA =)"}, {"=== h\na", "(H1 h #h)(PARA a)"}, {"=== h i {-}", "(H1 h i #h-i)[ATTR -]"}, {"=== h {{a}}", "(H1 h (EMBED a) #h)"}, {"=== h{{a}}", "(H1 h (EMBED a) #h)"}, {"=== {{a}}", "(H1 (EMBED a))"}, {"=== h {{a}}{-}", "(H1 h (EMBED a)[ATTR -] #h)"}, {"=== h {{a}} {-}", "(H1 h (EMBED a) #h)[ATTR -]"}, {"=== h {-}{{a}}", "(H1 h #h)[ATTR -]"}, {"=== h{id=abc}", "(H1 h #h)[ATTR id=abc]"}, {"=== h\n=== h", "(H1 h #h)(H1 h #h-1)"}, }) } func TestHRule(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"-", "(PARA -)"}, {"---", "(HR)"}, {"----", "(HR)"}, {"---A", "(HR)[ATTR =A]"}, {"---A-", "(HR)[ATTR =A-]"}, {"-1", "(PARA -1)"}, {"2-1", "(PARA 2-1)"}, {"--- { go } ", "(HR)[ATTR go]"}, {"--- { .go } ", "(HR)[ATTR class=go]"}, }) } func TestList(t *testing.T) { t.Parallel() // No ">" in the following, because quotation lists may have empty items. for _, ch := range []string{"*", "#"} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$ ", "(PARA $)"}, {"$$ ", "(PARA $$)"}, {"$$$ ", "(PARA $$$)"}, })) } checkTcs(t, TestCases{ {"* abc", "(UL {(PARA abc)})"}, {"** abc", "(UL {(UL {(PARA abc)})})"}, {"*** abc", "(UL {(UL {(UL {(PARA abc)})})})"}, {"**** abc", "(UL {(UL {(UL {(UL {(PARA abc)})})})})"}, {"** abc\n**** def", "(UL {(UL {(PARA abc)(UL {(UL {(PARA def)})})})})"}, {"* abc\ndef", "(UL {(PARA abc)})(PARA def)"}, {"* abc\n def", "(UL {(PARA abc)})(PARA def)"}, {"* abc\n* def", "(UL {(PARA abc)} {(PARA def)})"}, {"* abc\n def", "(UL {(PARA abc SB def)})"}, {"* abc\n def", "(UL {(PARA abc SB def)})"}, {"* abc\n\ndef", "(UL {(PARA abc)})(PARA def)"}, {"* abc\n\n def", "(UL {(PARA abc)})(PARA def)"}, {"* abc\n\n def", "(UL {(PARA abc)(PARA def)})"}, {"* abc\n\n def", "(UL {(PARA abc)(PARA def)})"}, {"* abc\n** def", "(UL {(PARA abc)(UL {(PARA def)})})"}, {"* abc\n** def\n* ghi", "(UL {(PARA abc)(UL {(PARA def)})} {(PARA ghi)})"}, {"* abc\n\n def\n* ghi", "(UL {(PARA abc)(PARA def)} {(PARA ghi)})"}, {"* abc\n** def\n ghi\n jkl", "(UL {(PARA abc)(UL {(PARA def SB ghi)})(PARA jkl)})"}, // A list does not last beyond a region {":::\n# abc\n:::\n# def", "(SPAN (OL {(PARA abc)}))(OL {(PARA def)})"}, // A HRule creates a new list {"* abc\n---\n* def", "(UL {(PARA abc)})(HR)(UL {(PARA def)})"}, // Changing list type adds a new list {"* abc\n# def", "(UL {(PARA abc)})(OL {(PARA def)})"}, // Quotation lists may have empty items {">", "(QL {})"}, }) } func TestQuoteList(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"> w1 w2", "(QL {(PARA w1 w2)})"}, {"> w1\n> w2", "(QL {(PARA w1 SB w2)})"}, {"> w1\n>\n>w2", "(QL {(PARA w1)} {})(PARA >w2)"}, }) } func TestEnumAfterPara(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"abc\n* def", "(PARA abc)(UL {(PARA def)})"}, {"abc\n*def", "(PARA abc SB *def)"}, }) } func TestDefinition(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {";", "(PARA ;)"}, {"; ", "(PARA ;)"}, {"; abc", "(DL (DT abc))"}, {"; abc\ndef", "(DL (DT abc))(PARA def)"}, {"; abc\n def", "(DL (DT abc))(PARA def)"}, {"; abc\n def", "(DL (DT abc SB def))"}, {":", "(PARA :)"}, {": ", "(PARA :)"}, {": abc", "(PARA : abc)"}, {"; abc\n: def", "(DL (DT abc) (DD (PARA def)))"}, {"; abc\n: def\nghi", "(DL (DT abc) (DD (PARA def)))(PARA ghi)"}, {"; abc\n: def\n ghi", "(DL (DT abc) (DD (PARA def)))(PARA ghi)"}, {"; abc\n: def\n ghi", "(DL (DT abc) (DD (PARA def SB ghi)))"}, {"; abc\n: def\n\n ghi", "(DL (DT abc) (DD (PARA def)(PARA ghi)))"}, {"; abc\n:", "(DL (DT abc))(PARA :)"}, {"; abc\n: def\n: ghi", "(DL (DT abc) (DD (PARA def)) (DD (PARA ghi)))"}, {"; abc\n: def\n; ghi\n: jkl", "(DL (DT abc) (DD (PARA def)) (DT ghi) (DD (PARA jkl)))"}, }) } func TestTable(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"|", "(TAB (TR))"}, {"||", "(TAB (TR (TD)))"}, {"| |", "(TAB (TR (TD)))"}, {"|a", "(TAB (TR (TD a)))"}, {"|a|", "(TAB (TR (TD a)))"}, {"|a| ", "(TAB (TR (TD a)(TD)))"}, {"|a|b", "(TAB (TR (TD a)(TD b)))"}, {"|a|b\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"}, {"|%", ""}, {"|a|b\n|%---\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"}, {"|a|b\n|c", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD)))"}, }) } func TestTransclude(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"{{{a}}}", "(TRANSCLUDE a)"}, {"{{{a}}}b", "(TRANSCLUDE a)[ATTR =b]"}, {"{{{a}}}}", "(TRANSCLUDE a)"}, {"{{{a\\}}}}", "(TRANSCLUDE a%5C%7D)"}, {"{{{a\\}}}}b", "(TRANSCLUDE a%5C%7D)[ATTR =b]"}, {"{{{a}}", "(PARA { (EMBED a))"}, {"{{{a}}}{go=b}", "(TRANSCLUDE a)[ATTR go=b]"}, }) } func TestBlockAttr(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {":::go\na\n:::", "(SPAN (PARA a))[ATTR =go]"}, {":::go=\na\n:::", "(SPAN (PARA a))[ATTR =go]"}, {":::{}\na\n:::", "(SPAN (PARA a))"}, {":::{ }\na\n:::", "(SPAN (PARA a))"}, {":::{.go}\na\n:::", "(SPAN (PARA a))[ATTR class=go]"}, {":::{=go}\na\n:::", "(SPAN (PARA a))[ATTR =go]"}, {":::{go}\na\n:::", "(SPAN (PARA a))[ATTR go]"}, {":::{go=py}\na\n:::", "(SPAN (PARA a))[ATTR go=py]"}, {":::{.go=py}\na\n:::", "(SPAN (PARA a))"}, {":::{go=}\na\n:::", "(SPAN (PARA a))[ATTR go]"}, {":::{.go=}\na\n:::", "(SPAN (PARA a))"}, {":::{go py}\na\n:::", "(SPAN (PARA a))[ATTR go py]"}, {":::{go\npy}\na\n:::", "(SPAN (PARA a))[ATTR go py]"}, {":::{.go py}\na\n:::", "(SPAN (PARA a))[ATTR class=go py]"}, {":::{go .py}\na\n:::", "(SPAN (PARA a))[ATTR class=py go]"}, {":::{.go py=3}\na\n:::", "(SPAN (PARA a))[ATTR class=go py=3]"}, {"::: { go } \na\n:::", "(SPAN (PARA a))[ATTR go]"}, {"::: { .go } \na\n:::", "(SPAN (PARA a))[ATTR class=go]"}, }) checkTcs(t, replace("\"", TestCases{ {":::{py=3}\na\n:::", "(SPAN (PARA a))[ATTR py=3]"}, {":::{py=$2 3$}\na\n:::", "(SPAN (PARA a))[ATTR py=$2 3$]"}, {":::{py=$2\\$3$}\na\n:::", "(SPAN (PARA a))[ATTR py=2$3]"}, {":::{py=2$3}\na\n:::", "(SPAN (PARA a))[ATTR py=2$3]"}, {":::{py=$2\n3$}\na\n:::", "(SPAN (PARA a))[ATTR py=$2\n3$]"}, {":::{py=$2 3}\na\n:::", "(SPAN (PARA a))"}, {":::{py=2 py=3}\na\n:::", "(SPAN (PARA a))[ATTR py=$2 3$]"}, {":::{.go .py}\na\n:::", "(SPAN (PARA a))[ATTR class=$go py$]"}, {":::{go go}\na\n:::", "(SPAN (PARA a))[ATTR go]"}, {":::{=py =go}\na\n:::", "(SPAN (PARA a))[ATTR =go]"}, })) } func TestInlineAttr(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"::a::{}", "(PARA {: a})"}, {"::a::{ }", "(PARA {: a})"}, {"::a::{.go}", "(PARA {: a}[ATTR class=go])"}, {"::a::{=go}", "(PARA {: a}[ATTR =go])"}, {"::a::{go}", "(PARA {: a}[ATTR go])"}, {"::a::{go=py}", "(PARA {: a}[ATTR go=py])"}, {"::a::{.go=py}", "(PARA {: a} {.go=py})"}, {"::a::{go=}", "(PARA {: a}[ATTR go])"}, {"::a::{.go=}", "(PARA {: a} {.go=})"}, {"::a::{go py}", "(PARA {: a}[ATTR go py])"}, {"::a::{go\npy}", "(PARA {: a}[ATTR go py])"}, {"::a::{.go py}", "(PARA {: a}[ATTR class=go py])"}, {"::a::{go .py}", "(PARA {: a}[ATTR class=py go])"}, {"::a::{ \n go \n .py\n \n}", "(PARA {: a}[ATTR class=py go])"}, {"::a::{ \n go \n .py\n\n}", "(PARA {: a}[ATTR class=py go])"}, {"::a::{\ngo\n}", "(PARA {: a}[ATTR go])"}, }) checkTcs(t, replace("\"", TestCases{ {"::a::{py=3}", "(PARA {: a}[ATTR py=3])"}, {"::a::{py=$2 3$}", "(PARA {: a}[ATTR py=$2 3$])"}, {"::a::{py=$2\\$3$}", "(PARA {: a}[ATTR py=2$3])"}, {"::a::{py=2$3}", "(PARA {: a}[ATTR py=2$3])"}, {"::a::{py=$2\n3$}", "(PARA {: a}[ATTR py=$2\n3$])"}, {"::a::{py=$2 3}", "(PARA {: a} {py=$2 3})"}, {"::a::{py=2 py=3}", "(PARA {: a}[ATTR py=$2 3$])"}, {"::a::{.go .py}", "(PARA {: a}[ATTR class=$go py$])"}, })) } func TestTemp(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"", ""}, }) } // -------------------------------------------------------------------------- // TestVisitor serializes the abstract syntax tree to a string. type TestVisitor struct { sb strings.Builder } func (tv *TestVisitor) String() string { return tv.sb.String() } func (tv *TestVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.InlineSlice: tv.visitInlineSlice(n) case *ast.ParaNode: tv.sb.WriteString("(PARA") ast.Walk(tv, &n.Inlines) tv.sb.WriteByte(')') case *ast.VerbatimNode: code, ok := mapVerbatimKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim code %v", n.Kind)) } tv.sb.WriteString(code) if len(n.Content) > 0 { tv.sb.WriteByte('\n') tv.sb.Write(n.Content) } tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.RegionNode: code, ok := mapRegionKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown region code %v", n.Kind)) } tv.sb.WriteString(code) if len(n.Blocks) > 0 { tv.sb.WriteByte(' ') ast.Walk(tv, &n.Blocks) } if len(n.Inlines) > 0 { tv.sb.WriteString(" (LINE") ast.Walk(tv, &n.Inlines) tv.sb.WriteByte(')') } tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.HeadingNode: fmt.Fprintf(&tv.sb, "(H%d", n.Level) ast.Walk(tv, &n.Inlines) if n.Fragment != "" { tv.sb.WriteString(" #") tv.sb.WriteString(n.Fragment) } tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.HRuleNode: tv.sb.WriteString("(HR)") tv.visitAttributes(n.Attrs) case *ast.NestedListNode: tv.sb.WriteString(mapNestedListKind[n.Kind]) for _, item := range n.Items { tv.sb.WriteString(" {") ast.WalkItemSlice(tv, item) tv.sb.WriteByte('}') } tv.sb.WriteByte(')') case *ast.DescriptionListNode: tv.sb.WriteString("(DL") for _, def := range n.Descriptions { tv.sb.WriteString(" (DT") ast.Walk(tv, &def.Term) tv.sb.WriteByte(')') for _, b := range def.Descriptions { tv.sb.WriteString(" (DD ") ast.WalkDescriptionSlice(tv, b) tv.sb.WriteByte(')') } } tv.sb.WriteByte(')') case *ast.TableNode: tv.sb.WriteString("(TAB") if len(n.Header) > 0 { tv.sb.WriteString(" (TR") for _, cell := range n.Header { tv.sb.WriteString(" (TH") tv.sb.WriteString(alignString[cell.Align]) ast.Walk(tv, &cell.Inlines) tv.sb.WriteString(")") } tv.sb.WriteString(")") } if len(n.Rows) > 0 { tv.sb.WriteString(" ") for _, row := range n.Rows { tv.sb.WriteString("(TR") for i, cell := range row { if i == 0 { tv.sb.WriteString(" ") } tv.sb.WriteString("(TD") tv.sb.WriteString(alignString[cell.Align]) ast.Walk(tv, &cell.Inlines) tv.sb.WriteString(")") } tv.sb.WriteString(")") } } tv.sb.WriteString(")") case *ast.TranscludeNode: fmt.Fprintf(&tv.sb, "(TRANSCLUDE %v)", n.Ref) // FIXME n.Inlines tv.visitAttributes(n.Attrs) case *ast.BLOBNode: tv.sb.WriteString("(BLOB ") tv.sb.WriteString(n.Syntax) tv.sb.WriteString(")") case *ast.TextNode: tv.sb.WriteString(n.Text) case *ast.BreakNode: if n.Hard { tv.sb.WriteString("HB") } else { tv.sb.WriteString("SB") } case *ast.LinkNode: fmt.Fprintf(&tv.sb, "(LINK %v", n.Ref) ast.Walk(tv, &n.Inlines) tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.EmbedRefNode: fmt.Fprintf(&tv.sb, "(EMBED %v", n.Ref) if len(n.Inlines) > 0 { ast.Walk(tv, &n.Inlines) } tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.EmbedBLOBNode: panic("TODO: zmktest blob") case *ast.CiteNode: fmt.Fprintf(&tv.sb, "(CITE %s", n.Key) if len(n.Inlines) > 0 { ast.Walk(tv, &n.Inlines) } tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.FootnoteNode: tv.sb.WriteString("(FN") ast.Walk(tv, &n.Inlines) tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.MarkNode: tv.sb.WriteString("(MARK") if n.Mark != "" { tv.sb.WriteString(" \"") tv.sb.WriteString(n.Mark) tv.sb.WriteByte('"') } if n.Fragment != "" { tv.sb.WriteString(" #") tv.sb.WriteString(n.Fragment) } if len(n.Inlines) > 0 { ast.Walk(tv, &n.Inlines) } tv.sb.WriteByte(')') case *ast.FormatNode: fmt.Fprintf(&tv.sb, "{%c", mapFormatKind[n.Kind]) ast.Walk(tv, &n.Inlines) tv.sb.WriteByte('}') tv.visitAttributes(n.Attrs) case *ast.LiteralNode: code, ok := mapLiteralKind[n.Kind] if !ok { panic(fmt.Sprintf("No element for code %v", n.Kind)) } tv.sb.WriteByte('{') tv.sb.WriteRune(code) if len(n.Content) > 0 { tv.sb.WriteByte(' ') tv.sb.Write(n.Content) } tv.sb.WriteByte('}') tv.visitAttributes(n.Attrs) default: return tv } return nil } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: "(ZETTEL", ast.VerbatimCode: "(PROG", ast.VerbatimEval: "(EVAL", ast.VerbatimMath: "(MATH", ast.VerbatimComment: "(COMMENT", } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: "(SPAN", ast.RegionQuote: "(QUOTE", ast.RegionVerse: "(VERSE", } var mapNestedListKind = map[ast.NestedListKind]string{ ast.NestedListOrdered: "(OL", ast.NestedListUnordered: "(UL", ast.NestedListQuote: "(QL", } var alignString = map[ast.Alignment]string{ ast.AlignDefault: "", ast.AlignLeft: "l", ast.AlignCenter: "c", ast.AlignRight: "r", } var mapFormatKind = map[ast.FormatKind]rune{ ast.FormatEmph: '_', ast.FormatStrong: '*', ast.FormatInsert: '>', ast.FormatDelete: '~', ast.FormatSuper: '^', ast.FormatSub: ',', ast.FormatQuote: '"', ast.FormatMark: '#', ast.FormatSpan: ':', } var mapLiteralKind = map[ast.LiteralKind]rune{ ast.LiteralCode: '`', ast.LiteralInput: '\'', ast.LiteralOutput: '=', ast.LiteralComment: '%', ast.LiteralMath: '$', } func (tv *TestVisitor) visitInlineSlice(is *ast.InlineSlice) { for _, in := range *is { tv.sb.WriteByte(' ') ast.Walk(tv, in) } } func (tv *TestVisitor) visitAttributes(a zsx.Attributes) { if a.IsEmpty() { return } tv.sb.WriteString("[ATTR") for _, k := range a.Keys() { tv.sb.WriteByte(' ') tv.sb.WriteString(k) v := a[k] if len(v) > 0 { tv.sb.WriteByte('=') if quoteString(v) { tv.sb.WriteByte('"') tv.sb.WriteString(v) tv.sb.WriteByte('"') } else { tv.sb.WriteString(v) } } } tv.sb.WriteByte(']') } func quoteString(s string) bool { for _, ch := range s { if ch <= ' ' { return true } } return false } |
Added internal/query/compiled.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | //----------------------------------------------------------------------------- // Copyright (c) 2023-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "math/rand/v2" "slices" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" ) // Compiled is a compiled query, to be used in a Box type Compiled struct { hasQuery bool seed int pick int order []sortOrder offset int // <= 0: no offset limit int // <= 0: no limit startMeta []*meta.Meta PreMatch MetaMatchFunc // Precondition for Match and Retrieve Terms []CompiledTerm sortFunc sortFunc } // MetaMatchFunc is a function determine whethe some metadata should be selected or not. type MetaMatchFunc func(*meta.Meta) bool func matchAlways(*meta.Meta) bool { return true } func matchNever(*meta.Meta) bool { return false } // CompiledTerm is the preprocessed sequence of conjugated search terms. type CompiledTerm struct { Match MetaMatchFunc // Match on metadata Retrieve RetrievePredicate // Retrieve from full-text search } // RetrievePredicate returns true, if the given Zid is contained in the (full-text) search. type RetrievePredicate func(id.Zid) bool // AlwaysIncluded is a RetrievePredicate that always returns true. func AlwaysIncluded(id.Zid) bool { return true } func neverIncluded(id.Zid) bool { return false } func (c *Compiled) isDeterministic() bool { return c.seed > 0 } // Result returns a result of the compiled search, that is achievable without iterating through a box. func (c *Compiled) Result() []*meta.Meta { if len(c.startMeta) == 0 { // nil -> no directive // empty slice -> nothing found return c.startMeta } result := make([]*meta.Meta, 0, len(c.startMeta)) for _, m := range c.startMeta { for _, term := range c.Terms { if term.Match(m) && term.Retrieve(m.Zid) { result = append(result, m) break } } } result = c.pickElements(result) c.ensureSortFunc() result = c.sortElements(result) result = c.offsetElements(result) return limitElements(result, c.limit) } func (c *Compiled) ensureSortFunc() { if c.sortFunc == nil { c.sortFunc = buildSortFunc(c.order) } } // AfterSearch applies all terms to the metadata list that was searched. // // This includes sorting, offset, limit, and picking. func (c *Compiled) AfterSearch(metaList []*meta.Meta) []*meta.Meta { if len(metaList) == 0 { return metaList } if !c.hasQuery { slices.SortFunc(metaList, defaultMetaSort) return metaList } if c.isDeterministic() { // We need to sort to make it deterministic if len(c.order) == 0 || c.order[0].isRandom() { slices.SortFunc(metaList, defaultMetaSort) } else { c.ensureSortFunc() slices.SortFunc(metaList, c.sortFunc) } } metaList = c.pickElements(metaList) if c.isDeterministic() { if len(c.order) > 0 && c.order[0].isRandom() { metaList = c.sortRandomly(metaList) } } else { metaList = c.sortElements(metaList) } metaList = c.offsetElements(metaList) return limitElements(metaList, c.limit) } func (c *Compiled) sortElements(metaList []*meta.Meta) []*meta.Meta { if len(c.order) > 0 { if c.order[0].isRandom() { metaList = c.sortRandomly(metaList) } else { c.ensureSortFunc() slices.SortFunc(metaList, c.sortFunc) } } return metaList } func (c *Compiled) offsetElements(metaList []*meta.Meta) []*meta.Meta { if c.offset == 0 { return metaList } if c.offset > len(metaList) { return nil } return metaList[c.offset:] } func (c *Compiled) pickElements(metaList []*meta.Meta) []*meta.Meta { count := c.pick if count <= 0 || count >= len(metaList) { return metaList } if limit := c.limit; limit > 0 && limit < count { count = limit c.limit = 0 } order := make([]int, len(metaList)) for i := range len(metaList) { order[i] = i } rnd := c.newRandom() picked := make([]int, count) for i := range count { last := len(order) - i n := rnd.IntN(last) picked[i] = order[n] order[n] = order[last-1] } order = nil slices.Sort(picked) result := make([]*meta.Meta, count) for i, p := range picked { result[i] = metaList[p] } return result } func (c *Compiled) sortRandomly(metaList []*meta.Meta) []*meta.Meta { rnd := c.newRandom() rnd.Shuffle( len(metaList), func(i, j int) { metaList[i], metaList[j] = metaList[j], metaList[i] }, ) return metaList } func (c *Compiled) newRandom() *rand.Rand { seed := c.seed if seed <= 0 { seed = rand.IntN(10000) + 10001 } return rand.New(rand.NewPCG(uint64(seed), uint64(seed))) } func limitElements(metaList []*meta.Meta, limit int) []*meta.Meta { if limit > 0 && limit < len(metaList) { return metaList[:limit] } return metaList } |
Added internal/query/context.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 | //----------------------------------------------------------------------------- // Copyright (c) 2023-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "container/heap" "context" "iter" "math" "slices" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" ) // ContextSpec contains all specification values for calculating a context. type ContextSpec struct { Direction ContextDirection MaxCost int MaxCount int MinCount int Full bool } // ContextDirection specifies the direction a context should be calculated. type ContextDirection uint8 // Constants for ContextDirection. const ( ContextDirBoth ContextDirection = iota ContextDirForward ContextDirBackward ) // ContextPort is the collection of box methods needed by this directive. type ContextPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *Query) ([]*meta.Meta, error) } // Print the spec on the given print environment. func (spec *ContextSpec) Print(pe *PrintEnv) { pe.printSpace() pe.writeString(api.ContextDirective) if spec.Full { pe.printSpace() pe.writeString(api.FullDirective) } switch spec.Direction { case ContextDirBackward: pe.printSpace() pe.writeString(api.BackwardDirective) case ContextDirForward: pe.printSpace() pe.writeString(api.ForwardDirective) } pe.printPosInt(api.CostDirective, spec.MaxCost) pe.printPosInt(api.MaxDirective, spec.MaxCount) pe.printPosInt(api.MinDirective, spec.MinCount) } // Execute the specification. func (spec *ContextSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ContextPort) []*meta.Meta { maxCost := float64(spec.MaxCost) if maxCost <= 0 { maxCost = 17 } maxCount := spec.MaxCount if maxCount <= 0 { maxCount = 200 } tasks := newQueue(startSeq, maxCost, maxCount, spec.MinCount, port) isBackward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirBackward isForward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirForward result := make([]*meta.Meta, 0, max(spec.MinCount, 16)) for { m, cost, level := tasks.next() if m == nil { break } result = append(result, m) for key, val := range m.ComputedRest() { tasks.addPair(ctx, key, val, cost, level, isBackward, isForward) } if !spec.Full { continue } tasks.addTags(ctx, m.GetFields(meta.KeyTags), cost, level) } return result } type ztlCtxItem struct { cost float64 meta *meta.Meta level uint } type ztlCtxQueue []ztlCtxItem func (q ztlCtxQueue) Len() int { return len(q) } func (q ztlCtxQueue) Less(i, j int) bool { levelI, levelJ := q[i].level, q[j].level if levelI == 0 { if levelJ == 0 { return q[i].meta.Zid < q[j].meta.Zid } return true } if levelI == 1 { if levelJ == 0 { return false } if levelJ == 1 { costI, costJ := q[i].cost, q[j].cost if costI == costJ { return q[i].meta.Zid < q[j].meta.Zid } return costI < costJ } return true } if levelJ == 0 || levelJ == 1 { return false } costI, costJ := q[i].cost, q[j].cost if costI == costJ { return q[i].meta.Zid < q[j].meta.Zid } return costI < costJ } func (q ztlCtxQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i] } func (q *ztlCtxQueue) Push(x any) { *q = append(*q, x.(ztlCtxItem)) } func (q *ztlCtxQueue) Pop() any { old := *q n := len(old) item := old[n-1] old[n-1].meta = nil // avoid memory leak *q = old[0 : n-1] return item } type contextTask struct { port ContextPort seen *idset.Set queue ztlCtxQueue maxCost float64 maxCount int minCount int tagMetas map[string][]*meta.Meta tagZids map[string]*idset.Set // just the zids of tagMetas metaZid map[id.Zid]*meta.Meta // maps zid to meta for all meta retrieved with tags } func newQueue(startSeq []*meta.Meta, maxCost float64, maxCount, minCount int, port ContextPort) *contextTask { result := &contextTask{ port: port, seen: idset.New(), maxCost: maxCost, maxCount: max(maxCount, minCount), minCount: minCount, tagMetas: make(map[string][]*meta.Meta), tagZids: make(map[string]*idset.Set), metaZid: make(map[id.Zid]*meta.Meta), } queue := make(ztlCtxQueue, 0, len(startSeq)) for _, m := range startSeq { queue = append(queue, ztlCtxItem{cost: 1, meta: m}) } heap.Init(&queue) result.queue = queue return result } func (ct *contextTask) addPair(ctx context.Context, key string, value meta.Value, curCost float64, level uint, isBackward, isForward bool) { if key == meta.KeyBack { return } newCost := curCost + contextCost(key) if key == meta.KeyBackward { if isBackward { ct.addIDSet(ctx, newCost, level, value) } return } if key == meta.KeyForward { if isForward { ct.addIDSet(ctx, newCost, level, value) } return } hasInverse := meta.Inverse(key) != "" if (!hasInverse || !isBackward) && (hasInverse || !isForward) { return } if t := meta.Type(key); t == meta.TypeID { ct.addID(ctx, newCost, level, value) } else if t == meta.TypeIDSet { ct.addIDSet(ctx, newCost, level, value) } } func contextCost(key string) float64 { switch key { case meta.KeyFolge, meta.KeyPrecursor: return 0.1 case meta.KeySequel, meta.KeyPrequel: return 1.0 case meta.KeySuccessors, meta.KeyPredecessor: return 7 } return 2 } func (ct *contextTask) addID(ctx context.Context, newCost float64, level uint, value meta.Value) { if zid, errParse := id.Parse(string(value)); errParse == nil { if m, errGetMeta := ct.port.GetMeta(ctx, zid); errGetMeta == nil { ct.addMeta(m, newCost, level) } } } func (ct *contextTask) addMeta(m *meta.Meta, newCost float64, level uint) { if !ct.seen.Contains(m.Zid) { heap.Push(&ct.queue, ztlCtxItem{cost: newCost, meta: m, level: level + 1}) } } func (ct *contextTask) addIDSet(ctx context.Context, newCost float64, level uint, value meta.Value) { elems := value.AsSlice() refCost := referenceCost(newCost, len(elems)) for _, val := range elems { ct.addID(ctx, refCost, level, meta.Value(val)) } } func referenceCost(baseCost float64, numReferences int) float64 { nRefs := float64(numReferences) return nRefs*math.Log2(nRefs+1) + baseCost - 1 } func (ct *contextTask) addTags(ctx context.Context, tagiter iter.Seq[string], baseCost float64, level uint) { tags := slices.Collect(tagiter) var zidSet *idset.Set for _, tag := range tags { zs := ct.updateTagData(ctx, tag) zidSet = zidSet.IUnion(zs) } zidSet.ForEach(func(zid id.Zid) { minCost := math.MaxFloat64 costFactor := 1.1 for _, tag := range tags { tagZids := ct.tagZids[tag] if tagZids.Contains(zid) { cost := tagCost(baseCost, tagZids.Length()) if cost < minCost { minCost = cost } costFactor /= 1.1 } } ct.addMeta(ct.metaZid[zid], minCost*costFactor, level) }) } func (ct *contextTask) updateTagData(ctx context.Context, tag string) *idset.Set { if _, found := ct.tagMetas[tag]; found { return ct.tagZids[tag] } q := Parse(meta.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + meta.KeyID) ml, err := ct.port.SelectMeta(ctx, nil, q) if err != nil { ml = nil } ct.tagMetas[tag] = ml zids := idset.NewCap(len(ml)) for _, m := range ml { zid := m.Zid zids = zids.Add(zid) if _, found := ct.metaZid[zid]; !found { ct.metaZid[zid] = m } } ct.tagZids[tag] = zids return zids } func tagCost(baseCost float64, numTags int) float64 { nTags := float64(numTags) return nTags*math.Log2(nTags+1) + baseCost - 1 } func (ct *contextTask) next() (*meta.Meta, float64, uint) { for len(ct.queue) > 0 { item := heap.Pop(&ct.queue).(ztlCtxItem) m := item.meta zid := m.Zid if ct.seen.Contains(zid) { continue } cost, level := item.cost, item.level if ct.hasEnough(cost, level) { break } ct.seen.Add(zid) return m, cost, item.level } return nil, -1, 0 } func (ct *contextTask) hasEnough(cost float64, level uint) bool { if level <= 1 { // Always add direct descendants of the initial zettel return false } length := ct.seen.Length() if minCount := ct.minCount; 0 < minCount && minCount > length { return false } if maxCount := ct.maxCount; 0 < maxCount && maxCount <= length { return true } maxCost := ct.maxCost return maxCost == 0.0 || maxCost <= cost } |
Added internal/query/parser.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "strconv" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" ) // Parse the query specification and return a Query object. func Parse(spec string) (q *Query) { return q.Parse(spec) } // Parse the query string and update the Query object. func (q *Query) Parse(spec string) *Query { state := parserState{ inp: input.NewInput([]byte(spec)), } q = state.parse(q) if q != nil { for len(q.terms) > 1 && q.terms[len(q.terms)-1].isEmpty() { q.terms = q.terms[:len(q.terms)-1] } } return q } type parserState struct { inp *input.Input } func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS } func (ps *parserState) acceptSingleKw(s string) bool { inp := ps.inp if inp.Accept(s) && (inp.IsSpace() || ps.isActionSep() || ps.mustStop()) { return true } return false } func (ps *parserState) acceptKwArgs(s string) bool { inp := ps.inp if inp.Accept(s) && inp.IsSpace() { inp.SkipSpace() return true } return false } const ( actionSeparatorChar = '|' existOperatorChar = '?' searchOperatorNotChar = '!' searchOperatorEqualChar = '=' searchOperatorHasChar = ':' searchOperatorPrefixChar = '[' searchOperatorSuffixChar = ']' searchOperatorMatchChar = '~' searchOperatorLessChar = '<' searchOperatorGreaterChar = '>' ) func (ps *parserState) parse(q *Query) *Query { inp := ps.inp inp.SkipSpace() if ps.mustStop() { return q } firstPos := inp.Pos zidSet := idset.New() for { pos := inp.Pos zid, found := ps.scanZid() if !found { inp.SetPos(pos) break } if !zidSet.Contains(zid) { zidSet.Add(zid) q = createIfNeeded(q) q.zids = append(q.zids, zid) } inp.SkipSpace() if ps.mustStop() { q.zids = nil break } } hasContext := false for { inp.SkipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.acceptSingleKw(api.ContextDirective) { if hasContext { inp.SetPos(pos) break } q = ps.parseContext(q) hasContext = true continue } inp.SetPos(pos) if q == nil || len(q.zids) == 0 { break } if ps.acceptSingleKw(api.IdentDirective) { q.directives = append(q.directives, &IdentSpec{}) continue } inp.SetPos(pos) if ps.acceptSingleKw(api.ItemsDirective) { q.directives = append(q.directives, &ItemsSpec{}) continue } inp.SetPos(pos) if ps.acceptSingleKw(api.UnlinkedDirective) { q = ps.parseUnlinked(q) continue } inp.SetPos(pos) break } if q != nil && len(q.directives) == 0 { inp.SetPos(firstPos) // No directive -> restart at beginning q.zids = nil } for { inp.SkipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.acceptSingleKw(api.OrDirective) { q = createIfNeeded(q) if !q.terms[len(q.terms)-1].isEmpty() { q.terms = append(q.terms, conjTerms{}) } continue } inp.SetPos(pos) if ps.acceptSingleKw(api.RandomDirective) { q = createIfNeeded(q) if len(q.order) == 0 { q.order = []sortOrder{{"", false}} } continue } inp.SetPos(pos) if ps.acceptKwArgs(api.PickDirective) { if s, ok := ps.parsePick(q); ok { q = s continue } } inp.SetPos(pos) if ps.acceptKwArgs(api.OrderDirective) { if s, ok := ps.parseOrder(q); ok { q = s continue } } inp.SetPos(pos) if ps.acceptKwArgs(api.OffsetDirective) { if s, ok := ps.parseOffset(q); ok { q = s continue } } inp.SetPos(pos) if ps.acceptKwArgs(api.LimitDirective) { if s, ok := ps.parseLimit(q); ok { q = s continue } } inp.SetPos(pos) if ps.isActionSep() { q = ps.parseActions(q) break } q = ps.parseText(q) } return q } func (ps *parserState) parseContext(q *Query) *Query { inp := ps.inp spec := &ContextSpec{} for { inp.SkipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.acceptSingleKw(api.FullDirective) { spec.Full = true continue } inp.SetPos(pos) if ps.acceptSingleKw(api.BackwardDirective) { spec.Direction = ContextDirBackward continue } inp.SetPos(pos) if ps.acceptSingleKw(api.ForwardDirective) { spec.Direction = ContextDirForward continue } inp.SetPos(pos) if ps.acceptKwArgs(api.CostDirective) { if ps.parseCost(spec) { continue } } inp.SetPos(pos) if ps.acceptKwArgs(api.MaxDirective) { if ps.parseMaxCount(spec) { continue } } inp.SetPos(pos) if ps.acceptKwArgs(api.MinDirective) { if ps.parseMinCount(spec) { continue } } inp.SetPos(pos) break } q = createIfNeeded(q) q.directives = append(q.directives, spec) return q } func (ps *parserState) parseCost(spec *ContextSpec) bool { num, ok := ps.scanPosInt() if !ok { return false } if spec.MaxCost == 0 || spec.MaxCost >= num { spec.MaxCost = num } return true } func (ps *parserState) parseMaxCount(spec *ContextSpec) bool { num, ok := ps.scanPosInt() if !ok { return false } if spec.MaxCount == 0 || spec.MaxCount >= num { spec.MaxCount = num } return true } func (ps *parserState) parseMinCount(spec *ContextSpec) bool { num, ok := ps.scanPosInt() if !ok { return false } if spec.MinCount == 0 || spec.MinCount <= num { spec.MinCount = num } return true } func (ps *parserState) parseUnlinked(q *Query) *Query { inp := ps.inp spec := &UnlinkedSpec{} for { inp.SkipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.acceptKwArgs(api.PhraseDirective) { if word := ps.scanWord(); len(word) > 0 { spec.words = append(spec.words, string(word)) continue } } inp.SetPos(pos) break } q.directives = append(q.directives, spec) return q } func (ps *parserState) parsePick(q *Query) (*Query, bool) { num, ok := ps.scanPosInt() if !ok { return q, false } q = createIfNeeded(q) if q.pick == 0 || q.pick >= num { q.pick = num } return q, true } func (ps *parserState) parseOrder(q *Query) (*Query, bool) { reverse := false if ps.acceptKwArgs(api.ReverseDirective) { reverse = true } word := ps.scanWord() if len(word) == 0 { return q, false } if sWord := string(word); meta.KeyIsValid(sWord) { q = createIfNeeded(q) if len(q.order) == 1 && q.order[0].isRandom() { q.order = nil } q.order = append(q.order, sortOrder{sWord, reverse}) return q, true } return q, false } func (ps *parserState) parseOffset(q *Query) (*Query, bool) { num, ok := ps.scanPosInt() if !ok { return q, false } q = createIfNeeded(q) if q.offset <= num { q.offset = num } return q, true } func (ps *parserState) parseLimit(q *Query) (*Query, bool) { num, ok := ps.scanPosInt() if !ok { return q, false } q = createIfNeeded(q) if q.limit == 0 || q.limit >= num { q.limit = num } return q, true } func (ps *parserState) parseActions(q *Query) *Query { inp := ps.inp inp.Next() var words []string for { inp.SkipSpace() word := ps.scanWord() if len(word) == 0 { break } words = append(words, string(word)) } if len(words) > 0 { q = createIfNeeded(q) q.actions = words } return q } func (ps *parserState) parseText(q *Query) *Query { inp := ps.inp pos := inp.Pos op, hasOp := ps.scanSearchOp() if hasOp && (op == cmpExist || op == cmpNotExist) { inp.SetPos(pos) hasOp = false } text, key := ps.scanSearchTextOrKey(hasOp) if len(key) > 0 { // Assert: hasOp == false op, hasOp = ps.scanSearchOp() // Assert hasOp == true if op == cmpExist || op == cmpNotExist { if inp.IsSpace() || ps.isActionSep() || ps.mustStop() { return q.addKey(string(key), op) } ps.inp.SetPos(pos) hasOp = false text = ps.scanWord() key = nil } else { text = ps.scanWord() } } else if len(text) == 0 { // Only an empty search operation is found -> ignore it return q } q = createIfNeeded(q) if hasOp { if key == nil { q.addSearch(expValue{meta.Value(string(text)), op}) } else { last := len(q.terms) - 1 if q.terms[last].mvals == nil { q.terms[last].mvals = expMetaValues{string(key): {expValue{meta.Value(string(text)), op}}} } else { sKey := string(key) q.terms[last].mvals[sKey] = append(q.terms[last].mvals[sKey], expValue{meta.Value(string(text)), op}) } } } else { // Assert key == nil q.addSearch(expValue{meta.Value(string(text)), cmpMatch}) } return q } func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) { inp := ps.inp pos := inp.Pos allowKey := !hasOp for !inp.IsSpace() && !ps.isActionSep() && !ps.mustStop() { if allowKey { switch inp.Ch { case searchOperatorNotChar, existOperatorChar, searchOperatorEqualChar, searchOperatorHasChar, searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar, searchOperatorLessChar, searchOperatorGreaterChar: allowKey = false if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) { return nil, key } } } inp.Next() } return inp.Src[pos:inp.Pos], nil } func (ps *parserState) scanWord() []byte { inp := ps.inp pos := inp.Pos for !inp.IsSpace() && !ps.isActionSep() && !ps.mustStop() { inp.Next() } return inp.Src[pos:inp.Pos] } func (ps *parserState) scanPosInt() (int, bool) { word := ps.scanWord() if len(word) == 0 { return 0, false } uval, err := strconv.ParseUint(string(word), 10, 63) if err != nil { return 0, false } return int(uval), true } func (ps *parserState) scanZid() (id.Zid, bool) { word := ps.scanWord() if len(word) == 0 { return id.Invalid, false } uval, err := id.ParseUint(string(word)) if err != nil { return id.Invalid, false } zid := id.Zid(uval) return zid, zid.IsValid() } func (ps *parserState) scanSearchOp() (compareOp, bool) { inp := ps.inp ch := inp.Ch negate := false if ch == searchOperatorNotChar { ch = inp.Next() negate = true } op := cmpUnknown switch ch { case existOperatorChar: inp.Next() op = cmpExist case searchOperatorEqualChar: inp.Next() op = cmpEqual case searchOperatorHasChar: inp.Next() op = cmpHas case searchOperatorSuffixChar: inp.Next() op = cmpSuffix case searchOperatorPrefixChar: inp.Next() op = cmpPrefix case searchOperatorMatchChar: inp.Next() op = cmpMatch case searchOperatorLessChar: inp.Next() op = cmpLess case searchOperatorGreaterChar: inp.Next() op = cmpGreater default: if negate { return cmpNoMatch, true } return cmpUnknown, false } if negate { return op.negate(), true } return op, true } func (ps *parserState) isActionSep() bool { return ps.inp.Ch == actionSeparatorChar } |
Added internal/query/parser_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package query_test import ( "testing" "zettelstore.de/z/internal/query" ) func TestParser(t *testing.T) { t.Parallel() testcases := []struct { spec string exp string }{ {"1", "1"}, // Just a number will transform to search for that number in all zettel {"1 IDENT", "00000000000001 IDENT"}, {"IDENT", "IDENT"}, {"1 IDENT|REINDEX", "00000000000001 IDENT | REINDEX"}, {"1 ITEMS", "00000000000001 ITEMS"}, {"ITEMS", "ITEMS"}, {"CONTEXT", "CONTEXT"}, {"CONTEXT a", "CONTEXT a"}, {"0 CONTEXT", "0 CONTEXT"}, {"1 CONTEXT", "00000000000001 CONTEXT"}, {"1 CONTEXT CONTEXT", "00000000000001 CONTEXT CONTEXT"}, {"00000000000001 CONTEXT", "00000000000001 CONTEXT"}, {"100000000000001 CONTEXT", "100000000000001 CONTEXT"}, {"1 CONTEXT FULL", "00000000000001 CONTEXT FULL"}, {"1 CONTEXT BACKWARD", "00000000000001 CONTEXT BACKWARD"}, {"1 CONTEXT FORWARD", "00000000000001 CONTEXT FORWARD"}, {"1 CONTEXT COST ", "00000000000001 CONTEXT COST"}, {"1 CONTEXT COST 3", "00000000000001 CONTEXT COST 3"}, {"1 CONTEXT COST x", "00000000000001 CONTEXT COST x"}, {"1 CONTEXT MAX 5", "00000000000001 CONTEXT MAX 5"}, {"1 CONTEXT MAX y", "00000000000001 CONTEXT MAX y"}, {"1 CONTEXT MIN 7", "00000000000001 CONTEXT MIN 7"}, {"1 CONTEXT MIN y", "00000000000001 CONTEXT MIN y"}, {"1 CONTEXT MAX 5 COST 7", "00000000000001 CONTEXT COST 7 MAX 5"}, {"1 CONTEXT | N", "00000000000001 CONTEXT | N"}, {"1 1 CONTEXT", "00000000000001 CONTEXT"}, {"1 2 CONTEXT", "00000000000001 00000000000002 CONTEXT"}, {"2 1 CONTEXT", "00000000000002 00000000000001 CONTEXT"}, {"1 CONTEXT|N", "00000000000001 CONTEXT | N"}, {"CONTEXT 0", "CONTEXT 0"}, {"1 UNLINKED", "00000000000001 UNLINKED"}, {"UNLINKED", "UNLINKED"}, {"1 UNLINKED PHRASE", "00000000000001 UNLINKED PHRASE"}, {"1 UNLINKED PHRASE Zettel", "00000000000001 UNLINKED PHRASE Zettel"}, {"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"}, {"key?", "key?"}, {"key!?", "key!?"}, {"b key?", "key? b"}, {"b key!?", "key!? b"}, {"key?a", "key?a"}, {"key!?a", "key!?a"}, {"", ""}, {"!", ""}, {":", ""}, {"!:", ""}, {"[", ""}, {"![", ""}, {"]", ""}, {"!]", ""}, {"~", ""}, {"!~", ""}, {"<", ""}, {"!<", ""}, {">", ""}, {"!>", ""}, {`a`, `a`}, {`!a`, `!a`}, {`=a`, `=a`}, {`!=a`, `!=a`}, {`:a`, `:a`}, {`!:a`, `!:a`}, {`[a`, `[a`}, {`![a`, `![a`}, {`]a`, `]a`}, {`!]a`, `!]a`}, {`~a`, `a`}, {`!~a`, `!a`}, {`key=`, `key=`}, {`key!=`, `key!=`}, {`key:`, `key:`}, {`key!:`, `key!:`}, {`key[`, `key[`}, {`key![`, `key![`}, {`key]`, `key]`}, {`key!]`, `key!]`}, {`key~`, `key~`}, {`key!~`, `key!~`}, {`key<`, `key<`}, {`key!<`, `key!<`}, {`key>`, `key>`}, {`key!>`, `key!>`}, {`key=a`, `key=a`}, {`key!=a`, `key!=a`}, {`key:a`, `key:a`}, {`key!:a`, `key!:a`}, {`key[a`, `key[a`}, {`key![a`, `key![a`}, {`key]a`, `key]a`}, {`key!]a`, `key!]a`}, {`key~a`, `key~a`}, {`key!~a`, `key!~a`}, {`key<a`, `key<a`}, {`key!<a`, `key!<a`}, {`key>a`, `key>a`}, {`key!>a`, `key!>a`}, {`key1:a key2:b`, `key1:a key2:b`}, {`key1: key2:b`, `key1: key2:b`}, {"word key:a", "key:a word"}, {`PICK 3`, `PICK 3`}, {`PICK 9 PICK 11`, `PICK 9`}, {"PICK a", "PICK a"}, {`RANDOM`, `RANDOM`}, {`RANDOM a`, `a RANDOM`}, {`a RANDOM`, `a RANDOM`}, {`RANDOM RANDOM a`, `a RANDOM`}, {`RANDOMRANDOM a`, `RANDOMRANDOM a`}, {`a RANDOMRANDOM`, `a RANDOMRANDOM`}, {`ORDER`, `ORDER`}, {"ORDER a b", "b ORDER a"}, {"a ORDER", "a ORDER"}, {"ORDER %", "ORDER %"}, {"ORDER a %", "% ORDER a"}, {"ORDER REVERSE", "ORDER REVERSE"}, {"ORDER REVERSE a b", "b ORDER REVERSE a"}, {"a RANDOM ORDER b", "a ORDER b"}, {"a ORDER b RANDOM", "a ORDER b"}, {"OFFSET", "OFFSET"}, {"OFFSET a", "OFFSET a"}, {"OFFSET 10 a", "a OFFSET 10"}, {"OFFSET 01 a", "a OFFSET 1"}, {"OFFSET 0 a", "a"}, {"a OFFSET 0", "a"}, {"OFFSET 4 OFFSET 8", "OFFSET 8"}, {"OFFSET 8 OFFSET 4", "OFFSET 8"}, {"LIMIT", "LIMIT"}, {"LIMIT a", "LIMIT a"}, {"LIMIT 10 a", "a LIMIT 10"}, {"LIMIT 01 a", "a LIMIT 1"}, {"LIMIT 0 a", "a"}, {"a LIMIT 0", "a"}, {"LIMIT 4 LIMIT 8", "LIMIT 4"}, {"LIMIT 8 LIMIT 4", "LIMIT 4"}, {"OR", ""}, {"OR OR", ""}, {"a OR", "a"}, {"OR b", "b"}, {"OR a OR", "a"}, {"a OR b", "a OR b"}, {"|", ""}, {" | RANDOM", "| RANDOM"}, {"| RANDOM", "| RANDOM"}, {"a|a b ", "a | a b"}, } for i, tc := range testcases { got := query.Parse(tc.spec).String() if tc.exp != got { t.Errorf("%d: Parse(%q) does not yield %q, but got %q", i, tc.spec, tc.exp, got) continue } gotReparse := query.Parse(got).String() if gotReparse != got { t.Errorf("%d: Parse(%q) does not yield itself, but %q", i, got, gotReparse) } gotPipe := query.Parse(got + "|").String() if gotPipe != got { t.Errorf("%d: Parse(%q) does not yield itself, but %q", i, got+"|", gotReparse) } } } |
Added internal/query/print.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "io" "maps" "slices" "strconv" "strings" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" ) var op2string = map[compareOp]string{ cmpExist: api.ExistOperator, cmpNotExist: api.ExistNotOperator, cmpEqual: api.SearchOperatorEqual, cmpNotEqual: api.SearchOperatorNotEqual, cmpHas: api.SearchOperatorHas, cmpHasNot: api.SearchOperatorHasNot, cmpPrefix: api.SearchOperatorPrefix, cmpNoPrefix: api.SearchOperatorNoPrefix, cmpSuffix: api.SearchOperatorSuffix, cmpNoSuffix: api.SearchOperatorNoSuffix, cmpMatch: api.SearchOperatorMatch, cmpNoMatch: api.SearchOperatorNoMatch, cmpLess: api.SearchOperatorLess, cmpNoLess: api.SearchOperatorNotLess, cmpGreater: api.SearchOperatorGreater, cmpNoGreater: api.SearchOperatorNotGreater, } func (q *Query) String() string { var sb strings.Builder q.Print(&sb) return sb.String() } // Print the query in a parseable form. func (q *Query) Print(w io.Writer) { if q == nil { return } env := PrintEnv{w: w} env.printZids(q.zids) for _, d := range q.directives { d.Print(&env) } for i, term := range q.terms { if i > 0 { env.writeString(" OR") } for _, name := range slices.Sorted(maps.Keys(term.keys)) { env.printSpace() env.writeString(name) if op := term.keys[name]; op == cmpExist || op == cmpNotExist { env.writeString(op2string[op]) } else { env.writeStrings(api.ExistOperator, " ", name, api.ExistNotOperator) } } for _, name := range slices.Sorted(maps.Keys(term.mvals)) { env.printExprValues(name, term.mvals[name]) } if len(term.search) > 0 { env.printExprValues("", term.search) } } env.printPosInt(api.PickDirective, q.pick) env.printOrder(q.order) env.printPosInt(api.OffsetDirective, q.offset) env.printPosInt(api.LimitDirective, q.limit) env.printActions(q.actions) } // PrintEnv is an environment where queries are printed. type PrintEnv struct { w io.Writer space bool } var bsSpace = []byte{' '} func (pe *PrintEnv) printSpace() { if pe.space { pe.w.Write(bsSpace) return } pe.space = true } func (pe *PrintEnv) write(ch byte) { pe.w.Write([]byte{ch}) } func (pe *PrintEnv) writeString(s string) { io.WriteString(pe.w, s) } func (pe *PrintEnv) writeStrings(sSeq ...string) { for _, s := range sSeq { io.WriteString(pe.w, s) } } func (pe *PrintEnv) printZids(zids []id.Zid) { for i, zid := range zids { if i > 0 { pe.printSpace() } pe.writeString(zid.String()) pe.space = true } } func (pe *PrintEnv) printExprValues(key string, values []expValue) { for _, val := range values { pe.printSpace() pe.writeString(key) switch op := val.op; op { case cmpMatch: // An empty key signals a full-text search. Since "~" is the default op in this case, // it can be ignored. Therefore, print only "~" if there is a key. if key != "" { pe.writeString(api.SearchOperatorMatch) } case cmpNoMatch: // An empty key signals a full-text search. Since "!" is the shortcut for "!~", // it can be ignored. Therefore, print only "!~" if there is a key. if key == "" { pe.writeString(api.SearchOperatorNot) } else { pe.writeString(api.SearchOperatorNoMatch) } default: if s, found := op2string[op]; found { pe.writeString(s) } else { pe.writeString("%" + strconv.Itoa(int(op))) } } if s := string(val.value); s != "" { pe.writeString(s) } } } // Human returns the query as a human readable string. func (q *Query) Human() string { var sb strings.Builder q.PrintHuman(&sb) return sb.String() } // PrintHuman the query to a writer in a human readable form. func (q *Query) PrintHuman(w io.Writer) { if q == nil { return } env := PrintEnv{w: w} env.printZids(q.zids) for _, d := range q.directives { d.Print(&env) } for i, term := range q.terms { if i > 0 { env.writeString(" OR ") env.space = false } for _, name := range slices.Sorted(maps.Keys(term.keys)) { if env.space { env.writeString(" AND ") } env.writeString(name) switch term.keys[name] { case cmpExist: env.writeString(" EXIST") case cmpNotExist: env.writeString(" NOT EXIST") default: env.writeString(" IS SCHRÖDINGER'S CAT") } env.space = true } for _, name := range slices.Sorted(maps.Keys(term.mvals)) { if env.space { env.writeString(" AND ") } env.writeString(name) env.printHumanSelectExprValues(term.mvals[name]) env.space = true } if len(term.search) > 0 { if env.space { env.writeString(" ") } env.writeString("ANY") env.printHumanSelectExprValues(term.search) env.space = true } } env.printPosInt(api.PickDirective, q.pick) env.printOrder(q.order) env.printPosInt(api.OffsetDirective, q.offset) env.printPosInt(api.LimitDirective, q.limit) env.printActions(q.actions) } func (pe *PrintEnv) printHumanSelectExprValues(values []expValue) { if len(values) == 0 { pe.writeString(" MATCH ANY") return } for j, val := range values { if j > 0 { pe.writeString(" AND") } switch val.op { case cmpEqual: pe.writeString(" EQUALS ") case cmpNotEqual: pe.writeString(" EQUALS NOT ") case cmpHas: pe.writeString(" HAS ") case cmpHasNot: pe.writeString(" HAS NOT ") case cmpPrefix: pe.writeString(" PREFIX ") case cmpNoPrefix: pe.writeString(" NOT PREFIX ") case cmpSuffix: pe.writeString(" SUFFIX ") case cmpNoSuffix: pe.writeString(" NOT SUFFIX ") case cmpMatch: pe.writeString(" MATCH ") case cmpNoMatch: pe.writeString(" NOT MATCH ") case cmpLess: pe.writeString(" LESS ") case cmpNoLess: pe.writeString(" NOT LESS ") case cmpGreater: pe.writeString(" GREATER ") case cmpNoGreater: pe.writeString(" NOT GREATER ") default: pe.writeString(" MaTcH ") } if val.value == "" { pe.writeString("NOTHING") } else { pe.writeString(string(val.value)) } } } func (pe *PrintEnv) printOrder(order []sortOrder) { for _, o := range order { if o.isRandom() { pe.printSpace() pe.writeString(api.RandomDirective) continue } pe.printSpace() pe.writeString(api.OrderDirective) if o.descending { pe.printSpace() pe.writeString(api.ReverseDirective) } pe.printSpace() pe.writeString(o.key) } } func (pe *PrintEnv) printPosInt(key string, val int) { if val > 0 { pe.printSpace() pe.writeStrings(key, " ", strconv.Itoa(val)) } } func (pe *PrintEnv) printActions(words []string) { if len(words) > 0 { pe.printSpace() pe.write(actionSeparatorChar) for _, word := range words { pe.printSpace() pe.writeString(word) } } } |
Added internal/query/query.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package query provides a query for zettel. package query import ( "context" "maps" "math/rand/v2" "slices" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" ) // Searcher is used to select zettel identifier based on search criteria. type Searcher interface { // Select all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. SearchEqual(word string) *idset.Set // Select all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. SearchPrefix(prefix string) *idset.Set // Select all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. SearchSuffix(suffix string) *idset.Set // Select all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. SearchContains(s string) *idset.Set } // Query specifies a mechanism for querying zettel. type Query struct { // Präfixed zettel identifier. zids []id.Zid // Querydirectives, like CONTEXT, ... directives []Directive // Fields to be used for selecting preMatch MetaMatchFunc // Match that must be true terms []conjTerms // Allow to create predictable randomness seed int pick int // Randomly pick elements, <= 0: no pick // Fields to be used for sorting order []sortOrder offset int // <= 0: no offset limit int // <= 0: no limit // Execute specification actions []string } // GetZids returns a slide of all specified zettel identifier. func (q *Query) GetZids() []id.Zid { if q == nil || len(q.zids) == 0 { return nil } return slices.Clone(q.zids) } // Directive are executed to process the list of metadata. type Directive interface { Print(*PrintEnv) } // GetDirectives returns the slice of query directives. func (q *Query) GetDirectives() []Directive { if q == nil || len(q.directives) == 0 { return nil } result := make([]Directive, len(q.directives)) copy(result, q.directives) return result } type keyExistMap map[string]compareOp type expMetaValues map[string][]expValue type conjTerms struct { keys keyExistMap mvals expMetaValues // Expected values for a meta datum search []expValue // Search string } func (ct *conjTerms) isEmpty() bool { return len(ct.keys) == 0 && len(ct.mvals) == 0 && len(ct.search) == 0 } func (ct *conjTerms) addKey(key string, op compareOp) { if ct.keys == nil { ct.keys = map[string]compareOp{key: op} return } if prevOp, found := ct.keys[key]; found { if prevOp != op { ct.keys[key] = cmpUnknown } return } ct.keys[key] = op } func (ct *conjTerms) addSearch(val expValue) { ct.search = append(ct.search, val) } type sortOrder struct { key string descending bool } func (so *sortOrder) isRandom() bool { return so.key == "" } func createIfNeeded(q *Query) *Query { if q == nil { return &Query{ terms: []conjTerms{{}}, } } return q } // Clone the query value. func (q *Query) Clone() *Query { if q == nil { return nil } c := new(Query) c.zids = q.GetZids() c.directives = q.GetDirectives() c.preMatch = q.preMatch c.terms = make([]conjTerms, len(q.terms)) for i, term := range q.terms { c.terms[i].keys = maps.Clone(term.keys) c.terms[i].mvals = maps.Clone(term.mvals) c.terms[i].search = slices.Clone(term.search) } c.seed = q.seed c.pick = q.pick c.order = slices.Clone(q.order) c.offset = q.offset c.limit = q.limit c.actions = q.actions return c } type compareOp uint8 const ( cmpUnknown compareOp = iota cmpExist cmpNotExist cmpEqual cmpNotEqual cmpHas cmpHasNot cmpPrefix cmpNoPrefix cmpSuffix cmpNoSuffix cmpMatch cmpNoMatch cmpLess cmpNoLess cmpGreater cmpNoGreater ) var negateMap = map[compareOp]compareOp{ cmpUnknown: cmpUnknown, cmpExist: cmpNotExist, cmpEqual: cmpNotEqual, cmpNotEqual: cmpEqual, cmpHas: cmpHasNot, cmpHasNot: cmpHas, cmpPrefix: cmpNoPrefix, cmpNoPrefix: cmpPrefix, cmpSuffix: cmpNoSuffix, cmpNoSuffix: cmpSuffix, cmpMatch: cmpNoMatch, cmpNoMatch: cmpMatch, cmpLess: cmpNoLess, cmpNoLess: cmpLess, cmpGreater: cmpNoGreater, cmpNoGreater: cmpGreater, } func (op compareOp) negate() compareOp { return negateMap[op] } var negativeMap = map[compareOp]bool{ cmpNotExist: true, cmpNotEqual: true, cmpHasNot: true, cmpNoPrefix: true, cmpNoSuffix: true, cmpNoMatch: true, cmpNoLess: true, cmpNoGreater: true, } func (op compareOp) isNegated() bool { return negativeMap[op] } type expValue struct { value meta.Value op compareOp } func (q *Query) addSearch(val expValue) { q.terms[len(q.terms)-1].addSearch(val) } func (q *Query) addKey(key string, op compareOp) *Query { q = createIfNeeded(q) q.terms[len(q.terms)-1].addKey(key, op) return q } var missingMap = map[compareOp]bool{ cmpNotExist: true, cmpNotEqual: true, cmpHasNot: true, cmpNoMatch: true, } // GetMetaValues returns the slice of all values specified for a given metadata key. // If `withMissing` is true, all values are returned. Otherwise only those, // where the comparison operator will positively search for a value. func (q *Query) GetMetaValues(key string, withMissing bool) (vals []meta.Value) { if q == nil { return nil } for _, term := range q.terms { if mvs, hasMv := term.mvals[key]; hasMv { for _, ev := range mvs { if withMissing || !missingMap[ev.op] { vals = append(vals, ev.value) } } } } slices.Sort(vals) return slices.Compact(vals) } // SetPreMatch sets the pre-selection predicate. func (q *Query) SetPreMatch(preMatch MetaMatchFunc) *Query { q = createIfNeeded(q) if q.preMatch != nil { panic("search PreMatch already set") } q.preMatch = preMatch return q } // SetSeed sets a seed value. func (q *Query) SetSeed(seed int) *Query { q = createIfNeeded(q) q.seed = seed return q } // GetSeed returns the seed value if one was set. func (q *Query) GetSeed() (int, bool) { if q == nil { return 0, false } return q.seed, q.seed > 0 } // SetDeterministic signals that the result should be the same if the seed is the same. func (q *Query) SetDeterministic() *Query { q = createIfNeeded(q) if q.seed <= 0 { q.seed = int(rand.IntN(10000) + 1) } return q } // Actions returns the slice of action specifications func (q *Query) Actions() []string { if q == nil { return nil } return q.actions } // RemoveActions will remove the action part of a query. func (q *Query) RemoveActions() { if q != nil { q.actions = nil } } // EnrichNeeded returns true, if the query references a metadata key that // is calculated via metadata enrichments. func (q *Query) EnrichNeeded() bool { if q == nil { return false } if len(q.zids) > 0 { return true } if len(q.actions) > 0 { // Unknown, what an action may use. For examples: KEYS action uses all metadata. return true } for _, term := range q.terms { for key := range term.keys { if meta.IsProperty(key) { return true } } for key := range term.mvals { if meta.IsProperty(key) { return true } } } for _, o := range q.order { if meta.IsProperty(o.key) { return true } } return false } // RetrieveAndCompile queries the search index and returns a predicate // for its results and returns a matching predicate. func (q *Query) RetrieveAndCompile(_ context.Context, searcher Searcher, metaSeq []*meta.Meta) Compiled { if q == nil { return Compiled{ PreMatch: matchAlways, Terms: []CompiledTerm{{ Match: matchAlways, Retrieve: AlwaysIncluded, }}} } q = q.Clone() preMatch := q.preMatch if preMatch == nil { preMatch = matchAlways } startSet := metaList2idSet(metaSeq) result := Compiled{ hasQuery: true, seed: q.seed, pick: q.pick, order: q.order, offset: q.offset, limit: q.limit, startMeta: metaSeq, PreMatch: preMatch, Terms: []CompiledTerm{}, } for _, term := range q.terms { cTerm := term.retrieveAndCompileTerm(searcher, startSet) if cTerm.Retrieve == nil { if cTerm.Match == nil { // no restriction on match/retrieve -> all will match result.Terms = []CompiledTerm{{ Match: matchAlways, Retrieve: AlwaysIncluded, }} break } cTerm.Retrieve = AlwaysIncluded } if cTerm.Match == nil { cTerm.Match = matchAlways } result.Terms = append(result.Terms, cTerm) } return result } func metaList2idSet(ml []*meta.Meta) *idset.Set { if ml == nil { return nil } result := idset.NewCap(len(ml)) for _, m := range ml { result = result.Add(m.Zid) } return result } func (ct *conjTerms) retrieveAndCompileTerm(searcher Searcher, startSet *idset.Set) CompiledTerm { match := ct.compileMeta() // Match might add some searches var pred RetrievePredicate if searcher != nil { pred = ct.retrieveIndex(searcher) if startSet != nil { if pred == nil { pred = startSet.ContainsOrNil } else { predSet := idset.NewCap(startSet.Length()) startSet.ForEach(func(zid id.Zid) { if pred(zid) { predSet = predSet.Add(zid) } }) pred = predSet.ContainsOrNil } } } return CompiledTerm{Match: match, Retrieve: pred} } // retrieveIndex and return a predicate to ask for results. func (ct *conjTerms) retrieveIndex(searcher Searcher) RetrievePredicate { if len(ct.search) == 0 { return nil } normCalls, plainCalls, negCalls := prepareRetrieveCalls(searcher, ct.search) if hasConflictingCalls(normCalls, plainCalls, negCalls) { return neverIncluded } positives := retrievePositives(normCalls, plainCalls) if positives == nil { // No positive search for words, must contain only words for a negative search. // Otherwise len(search) == 0 (see above) negatives := retrieveNegatives(negCalls) return func(zid id.Zid) bool { return !negatives.ContainsOrNil(zid) } } if positives.IsEmpty() { // Positive search didn't found anything. We can omit the negative search. return neverIncluded } if len(negCalls) == 0 { // Positive search found something, but there is no negative search. return positives.Contains } negatives := retrieveNegatives(negCalls) if negatives == nil { return positives.Contains } return func(zid id.Zid) bool { return positives.Contains(zid) && !negatives.ContainsOrNil(zid) } } // Limit returns only s.GetLimit() elements of the given list. func (q *Query) Limit(metaList []*meta.Meta) []*meta.Meta { if q == nil { return metaList } return limitElements(metaList, q.limit) } |
Added internal/query/retrieve.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package query // This file contains helper functions to search within the index. import ( "fmt" "strings" "t73f.de/r/zsc/domain/id/idset" "zettelstore.de/z/strfun" ) type searchOp struct { s string op compareOp } type searchFunc func(string) *idset.Set type searchCallMap map[searchOp]searchFunc var cmpPred = map[compareOp]func(string, string) bool{ cmpEqual: stringEqual, cmpPrefix: strings.HasPrefix, cmpSuffix: strings.HasSuffix, cmpMatch: strings.Contains, cmpHas: strings.Contains, // the "has" operator have string semantics here in a index search cmpLess: strings.Contains, // in index search there is no "less", only "has" cmpGreater: strings.Contains, // in index search there is no "greater", only "has" } func (scm searchCallMap) addSearch(s string, op compareOp, sf searchFunc) { pred := cmpPred[op] for k := range scm { if op == cmpMatch { if strings.Contains(k.s, s) { return } if strings.Contains(s, k.s) { delete(scm, k) break } } if k.op != op { continue } if pred(k.s, s) { return } if pred(s, k.s) { delete(scm, k) } } scm[searchOp{s: s, op: op}] = sf } func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) { normCalls = make(searchCallMap, len(search)) negCalls = make(searchCallMap, len(search)) for _, val := range search { for _, word := range strfun.NormalizeWords(string(val.value)) { if cmpOp := val.op; cmpOp.isNegated() { cmpOp = cmpOp.negate() negCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) } else { normCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) } } } plainCalls = make(searchCallMap, len(search)) for _, val := range search { word := val.value.TrimSpace().ToLower() if cmpOp := val.op; cmpOp.isNegated() { cmpOp = cmpOp.negate() negCalls.addSearch(string(word), cmpOp, getSearchFunc(searcher, cmpOp)) } else { plainCalls.addSearch(string(word), cmpOp, getSearchFunc(searcher, cmpOp)) } } return normCalls, plainCalls, negCalls } func hasConflictingCalls(normCalls, plainCalls, negCalls searchCallMap) bool { for val := range negCalls { if _, found := normCalls[val]; found { return true } if _, found := plainCalls[val]; found { return true } } return false } func retrievePositives(normCalls, plainCalls searchCallMap) *idset.Set { if isSuperset(normCalls, plainCalls) { var normResult *idset.Set for c, sf := range normCalls { normResult = normResult.IntersectOrSet(sf(c.s)) } return normResult } cache := make(map[searchOp]*idset.Set) var plainResult *idset.Set for c, sf := range plainCalls { result := sf(c.s) if _, found := normCalls[c]; found { cache[c] = result } plainResult = plainResult.IntersectOrSet(result) } var normResult *idset.Set for c, sf := range normCalls { if result, found := cache[c]; found { normResult = normResult.IntersectOrSet(result) } else { normResult = normResult.IntersectOrSet(sf(c.s)) } } return normResult.IUnion(plainResult) } func isSuperset(normCalls, plainCalls searchCallMap) bool { for c := range plainCalls { if _, found := normCalls[c]; !found { return false } } return true } func retrieveNegatives(negCalls searchCallMap) *idset.Set { var negatives *idset.Set for val, sf := range negCalls { negatives = negatives.IUnion(sf(val.s)) } return negatives } func getSearchFunc(searcher Searcher, op compareOp) searchFunc { switch op { case cmpEqual: return searcher.SearchEqual case cmpPrefix: return searcher.SearchPrefix case cmpSuffix: return searcher.SearchSuffix case cmpMatch, cmpHas, cmpLess, cmpGreater: // for index search we assume string semantics return searcher.SearchContains default: panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op)) } } |
Added internal/query/select.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "fmt" "strconv" "strings" "t73f.de/r/zsc/domain/meta" ) type matchValueFunc func(value meta.Value) bool func matchValueNever(meta.Value) bool { return false } type matchSpec struct { key string match matchValueFunc } // compileMeta calculates a selection func based on the given select criteria. func (ct *conjTerms) compileMeta() MetaMatchFunc { for key, vals := range ct.mvals { // All queried keys must exist, if there is at least one non-negated compare operation // // This is only an optimization to make selection of metadata faster. if countNegatedOps(vals) < len(vals) { ct.addKey(key, cmpExist) } } for _, op := range ct.keys { if op != cmpExist && op != cmpNotExist { return matchNever } } posSpecs, negSpecs := ct.createSelectSpecs() if len(posSpecs) > 0 || len(negSpecs) > 0 || len(ct.keys) > 0 { return makeSearchMetaMatchFunc(posSpecs, negSpecs, ct.keys) } return nil } func countNegatedOps(vals []expValue) int { count := 0 for _, val := range vals { if val.op.isNegated() { count++ } } return count } func (ct *conjTerms) createSelectSpecs() (posSpecs, negSpecs []matchSpec) { posSpecs = make([]matchSpec, 0, len(ct.mvals)) negSpecs = make([]matchSpec, 0, len(ct.mvals)) for key, values := range ct.mvals { if !meta.KeyIsValid(key) { continue } posMatch, negMatch := createPosNegMatchFunc(key, values, ct.addSearch) if posMatch != nil { posSpecs = append(posSpecs, matchSpec{key, posMatch}) } if negMatch != nil { negSpecs = append(negSpecs, matchSpec{key, negMatch}) } } return posSpecs, negSpecs } type addSearchFunc func(val expValue) func noAddSearch(expValue) { /* Just does nothing, for negated queries */ } func createPosNegMatchFunc(key string, values []expValue, addSearch addSearchFunc) (posMatch, negMatch matchValueFunc) { posValues := make([]expValue, 0, len(values)) negValues := make([]expValue, 0, len(values)) for _, val := range values { if val.op.isNegated() { negValues = append(negValues, val) } else { posValues = append(posValues, val) } } if meta.IsProperty(key) { // Properties are not stored in the Zettelstore and in the search index. addSearch = noAddSearch } return createMatchFunc(key, posValues, addSearch), createMatchFunc(key, negValues, addSearch) } func createMatchFunc(key string, values []expValue, addSearch addSearchFunc) matchValueFunc { if len(values) == 0 { return nil } switch meta.Type(key) { case meta.TypeCredential: return matchValueNever case meta.TypeID: return createMatchIDFunc(values, addSearch) case meta.TypeIDSet: return createMatchIDSetFunc(values, addSearch) case meta.TypeTimestamp: return createMatchTimestampFunc(values, addSearch) case meta.TypeNumber: return createMatchNumberFunc(values, addSearch) case meta.TypeTagSet: return createMatchTagSetFunc(values, addSearch) case meta.TypeWord: return createMatchWordFunc(values, addSearch) } return createMatchStringFunc(values, addSearch) } func createMatchIDFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToIDPredicates(values, addSearch) return func(value meta.Value) bool { for _, pred := range preds { if !pred(value) { return false } } return true } } func createMatchIDSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { predList := valuesToSetPredicates(preprocessSet(values), addSearch) return func(value meta.Value) bool { ids := value.AsSlice() for _, preds := range predList { for _, pred := range preds { if !pred(ids) { return false } } } return true } } func createMatchTimestampFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToTimestampPredicates(values, addSearch) return func(value meta.Value) bool { value = meta.Value(meta.ExpandTimestamp(value)) for _, pred := range preds { if !pred(value) { return false } } return true } } func createMatchNumberFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToNumberPredicates(values, addSearch) return func(value meta.Value) bool { for _, pred := range preds { if !pred(value) { return false } } return true } } func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { predList := valuesToSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch) return func(value meta.Value) bool { tags := value.AsTags() for _, preds := range predList { for _, pred := range preds { if !pred(tags) { return false } } } return true } } func processTagSet(valueSet [][]expValue) [][]expValue { result := make([][]expValue, len(valueSet)) for i, values := range valueSet { tags := make([]expValue, len(values)) for j, val := range values { if tval := val.value; tval != "" && tval[0] == '#' { tval = tval.CleanTag() tags[j] = expValue{value: tval, op: val.op} } else { tags[j] = expValue{value: tval, op: val.op} } } result[i] = tags } return result } func createMatchWordFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToWordPredicates(sliceToLower(values), addSearch) return func(value meta.Value) bool { value = meta.Value(strings.ToLower(string(value))) for _, pred := range preds { if !pred(value) { return false } } return true } } func createMatchStringFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToStringPredicates(sliceToLower(values), addSearch) return func(value meta.Value) bool { value = meta.Value(strings.ToLower(string(value))) for _, pred := range preds { if !pred(value) { return false } } return true } } func sliceToLower(sl []expValue) []expValue { result := make([]expValue, 0, len(sl)) for _, s := range sl { result = append(result, expValue{ value: meta.Value(strings.ToLower(string(s.value))), op: s.op, }) } return result } func preprocessSet(set []expValue) [][]expValue { result := make([][]expValue, 0, len(set)) for _, elem := range set { splitElems := strings.Split(string(elem.value), ",") valueElems := make([]expValue, 0, len(splitElems)) for _, se := range splitElems { e := strings.TrimSpace(se) if len(e) > 0 { valueElems = append(valueElems, expValue{value: meta.Value(e), op: elem.op}) } } if len(valueElems) > 0 { result = append(result, valueElems) } } return result } type stringPredicate func(meta.Value) bool func valuesToIDPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { result := make([]stringPredicate, len(values)) for i, v := range values { value := v.value if len(value) > 14 { value = value[:14] } switch op := disambiguatedIDOp(v.op); op { case cmpLess, cmpNoLess, cmpGreater, cmpNoGreater: if isDigits(string(value)) { // Never add the strValue to search. // Append enough zeroes to make it comparable as string. // (an ID and a timestamp always have 14 digits) strValue := string(value) + "00000000000000"[:14-len(value)] result[i] = createIDCompareFunc(meta.Value(strValue), op) continue } fallthrough default: // Otherwise compare as a word. if !op.isNegated() { addSearch(v) // addSearch only for positive selections } result[i] = createWordCompareFunc(value, op) } } return result } func isDigits(s string) bool { for i := range len(s) { if ch := s[i]; ch < '0' || '9' < ch { return false } } return true } func disambiguatedIDOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) } func createIDCompareFunc(cmpVal meta.Value, cmpOp compareOp) stringPredicate { return createWordCompareFunc(cmpVal, cmpOp) } func valuesToTimestampPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { result := make([]stringPredicate, len(values)) for i, v := range values { value := meta.ExpandTimestamp(v.value) switch op := disambiguatedTimestampOp(v.op); op { case cmpLess, cmpNoLess, cmpGreater, cmpNoGreater: if isDigits(value) { // Never add the value to search. result[i] = createTimestampCompareFunc(meta.Value(value), op) continue } fallthrough default: // Otherwise compare as a word. if !op.isNegated() { addSearch(v) // addSearch only for positive selections } result[i] = createWordCompareFunc(meta.Value(value), op) } } return result } func disambiguatedTimestampOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) } func createTimestampCompareFunc(cmpVal meta.Value, cmpOp compareOp) stringPredicate { return createWordCompareFunc(cmpVal, cmpOp) } func valuesToNumberPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { result := make([]stringPredicate, len(values)) for i, v := range values { switch op := disambiguatedNumberOp(v.op); op { case cmpEqual, cmpNotEqual, cmpLess, cmpNoLess, cmpGreater, cmpNoGreater: iValue, err := strconv.ParseInt(string(v.value), 10, 64) if err == nil { // Never add the strValue to search. result[i] = createNumberCompareFunc(iValue, op) continue } fallthrough default: // In all other cases, a number is treated like a word. if !op.isNegated() { addSearch(v) // addSearch only for positive selections } result[i] = createWordCompareFunc(v.value, op) } } return result } func disambiguatedNumberOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) } func createNumberCompareFunc(cmpVal int64, cmpOp compareOp) stringPredicate { var cmpFunc func(int64) bool switch cmpOp { case cmpEqual: cmpFunc = func(iMetaVal int64) bool { return iMetaVal == cmpVal } case cmpNotEqual: cmpFunc = func(iMetaVal int64) bool { return iMetaVal != cmpVal } case cmpLess: cmpFunc = func(iMetaVal int64) bool { return iMetaVal < cmpVal } case cmpNoLess: cmpFunc = func(iMetaVal int64) bool { return iMetaVal >= cmpVal } case cmpGreater: cmpFunc = func(iMetaVal int64) bool { return iMetaVal > cmpVal } case cmpNoGreater: cmpFunc = func(iMetaVal int64) bool { return iMetaVal <= cmpVal } default: panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal)) } return func(metaVal meta.Value) bool { iMetaVal, err := strconv.ParseInt(string(metaVal), 10, 64) if err != nil { return false } return cmpFunc(iMetaVal) } } func valuesToStringPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { result := make([]stringPredicate, len(values)) for i, v := range values { op := disambiguatedStringOp(v.op) if !op.isNegated() { addSearch(v) // addSearch only for positive selections } result[i] = createStringCompareFunc(v.value, op) } return result } func disambiguatedStringOp(cmpOp compareOp) compareOp { switch cmpOp { case cmpHas: return cmpMatch case cmpHasNot: return cmpNoMatch default: return cmpOp } } func createStringCompareFunc(cmpVal meta.Value, cmpOp compareOp) stringPredicate { return createWordCompareFunc(cmpVal, cmpOp) } func valuesToWordPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { result := make([]stringPredicate, len(values)) for i, v := range values { op := disambiguateWordOp(v.op) if !op.isNegated() { addSearch(v) // addSearch only for positive selections } result[i] = createWordCompareFunc(v.value, op) } return result } func disambiguateWordOp(cmpOp compareOp) compareOp { switch cmpOp { case cmpHas: return cmpEqual case cmpHasNot: return cmpNotEqual default: return cmpOp } } func createWordCompareFunc(cmpVal meta.Value, cmpOp compareOp) stringPredicate { switch cmpOp { case cmpEqual: return func(metaVal meta.Value) bool { return metaVal == cmpVal } case cmpNotEqual: return func(metaVal meta.Value) bool { return metaVal != cmpVal } case cmpPrefix: return func(metaVal meta.Value) bool { return strings.HasPrefix(string(metaVal), string(cmpVal)) } case cmpNoPrefix: return func(metaVal meta.Value) bool { return !strings.HasPrefix(string(metaVal), string(cmpVal)) } case cmpSuffix: return func(metaVal meta.Value) bool { return strings.HasSuffix(string(metaVal), string(cmpVal)) } case cmpNoSuffix: return func(metaVal meta.Value) bool { return !strings.HasSuffix(string(metaVal), string(cmpVal)) } case cmpMatch: return func(metaVal meta.Value) bool { return strings.Contains(string(metaVal), string(cmpVal)) } case cmpNoMatch: return func(metaVal meta.Value) bool { return !strings.Contains(string(metaVal), string(cmpVal)) } case cmpLess: return func(metaVal meta.Value) bool { return metaVal < cmpVal } case cmpNoLess: return func(metaVal meta.Value) bool { return metaVal >= cmpVal } case cmpGreater: return func(metaVal meta.Value) bool { return metaVal > cmpVal } case cmpNoGreater: return func(metaVal meta.Value) bool { return metaVal <= cmpVal } case cmpHas, cmpHasNot: panic(fmt.Sprintf("operator %d not disambiguated with value %q", cmpOp, cmpVal)) default: panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal)) } } type stringSetPredicate func(value []string) bool func valuesToSetPredicates(values [][]expValue, addSearch addSearchFunc) [][]stringSetPredicate { result := make([][]stringSetPredicate, len(values)) for i, val := range values { elemPreds := make([]stringSetPredicate, len(val)) for j, v := range val { opVal := v.value // loop variable is used in closure --> save needed value switch op := disambiguateWordOp(v.op); op { case cmpEqual: addSearch(v) // addSearch only for positive selections fallthrough case cmpNotEqual: elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, op == cmpEqual) case cmpPrefix: addSearch(v) fallthrough case cmpNoPrefix: elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, op == cmpPrefix) case cmpSuffix: addSearch(v) fallthrough case cmpNoSuffix: elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, op == cmpSuffix) case cmpMatch: addSearch(v) fallthrough case cmpNoMatch: elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, op == cmpMatch) case cmpLess, cmpNoLess: elemPreds[j] = makeStringSetPredicate(opVal, stringLess, op == cmpLess) case cmpGreater, cmpNoGreater: elemPreds[j] = makeStringSetPredicate(opVal, stringGreater, op == cmpGreater) case cmpHas, cmpHasNot: panic(fmt.Sprintf("operator %d not disambiguated with value %q", op, opVal)) default: panic(fmt.Sprintf("Unknown compare operation %d with value %q", op, opVal)) } } result[i] = elemPreds } return result } func stringEqual(val1, val2 string) bool { return val1 == val2 } func stringLess(val1, val2 string) bool { return val1 < val2 } func stringGreater(val1, val2 string) bool { return val1 > val2 } type compareStringFunc func(val1, val2 string) bool func makeStringSetPredicate(neededValue meta.Value, compare compareStringFunc, foundResult bool) stringSetPredicate { return func(metaVals []string) bool { for _, metaVal := range metaVals { if compare(metaVal, string(neededValue)) { return foundResult } } return !foundResult } } func makeSearchMetaMatchFunc(posSpecs, negSpecs []matchSpec, kem keyExistMap) MetaMatchFunc { // Optimize: no specs --> just check kwhether key exists if len(posSpecs) == 0 && len(negSpecs) == 0 { if len(kem) == 0 { return nil } return func(m *meta.Meta) bool { return matchMetaKeyExists(m, kem) } } // Optimize: only negative or only positive matching if len(posSpecs) == 0 { return func(m *meta.Meta) bool { return matchMetaKeyExists(m, kem) && matchMetaSpecs(m, negSpecs) } } if len(negSpecs) == 0 { return func(m *meta.Meta) bool { return matchMetaKeyExists(m, kem) && matchMetaSpecs(m, posSpecs) } } return func(m *meta.Meta) bool { return matchMetaKeyExists(m, kem) && matchMetaSpecs(m, posSpecs) && matchMetaSpecs(m, negSpecs) } } func matchMetaKeyExists(m *meta.Meta, kem keyExistMap) bool { for key, op := range kem { _, found := m.Get(key) if found != (op == cmpExist) { return false } } return true } func matchMetaSpecs(m *meta.Meta, specs []matchSpec) bool { for _, s := range specs { if value := m.GetDefault(s.key, ""); !s.match(value) { return false } } return true } |
Added internal/query/select_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package query_test import ( "context" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/query" ) func TestMatchZidNegate(t *testing.T) { q := query.Parse(meta.KeyID + api.SearchOperatorHasNot + id.ZidVersion.String() + " " + meta.KeyID + api.SearchOperatorHasNot + id.ZidLicense.String()) compiled := q.RetrieveAndCompile(context.Background(), nil, nil) testCases := []struct { zid id.Zid exp bool }{ {id.ZidVersion, false}, {id.ZidLicense, false}, {id.ZidAuthors, true}, } for i, tc := range testCases { m := meta.New(tc.zid) if compiled.Terms[0].Match(m) != tc.exp { if tc.exp { t.Errorf("%d: meta %v must match %q", i, m.Zid, q) } else { t.Errorf("%d: meta %v must not match %q", i, m.Zid, q) } } } } |
Added internal/query/sorter.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "cmp" "strconv" "t73f.de/r/zsc/domain/meta" ) type sortFunc func(i, j *meta.Meta) int func buildSortFunc(order []sortOrder) sortFunc { hasID := false sortFuncs := make([]sortFunc, 0, len(order)+1) for _, o := range order { sortFuncs = append(sortFuncs, o.buildSortfunc()) if o.key == meta.KeyID { hasID = true break } } if !hasID { sortFuncs = append(sortFuncs, defaultMetaSort) } if len(sortFuncs) == 1 { return sortFuncs[0] } return func(i, j *meta.Meta) int { for _, sf := range sortFuncs { if result := sf(i, j); result != 0 { return result } } return 0 } } func (so *sortOrder) buildSortfunc() sortFunc { key := so.key keyType := meta.Type(key) if key == meta.KeyID || keyType == meta.TypeCredential { if so.descending { return defaultMetaSort } return func(i, j *meta.Meta) int { return cmp.Compare(i.Zid, j.Zid) } } if keyType == meta.TypeTimestamp { return createSortTimestampFunc(key, so.descending) } if keyType == meta.TypeNumber { return createSortNumberFunc(key, so.descending) } return createSortStringFunc(key, so.descending) } func defaultMetaSort(i, j *meta.Meta) int { return cmp.Compare(j.Zid, i.Zid) } func createSortTimestampFunc(key string, descending bool) sortFunc { if descending { return func(i, j *meta.Meta) int { iVal, iOk := i.Get(key) jVal, jOk := j.Get(key) if result := compareFound(jOk, iOk); result != 0 { return result } return cmp.Compare(meta.ExpandTimestamp(jVal), meta.ExpandTimestamp(iVal)) } } return func(i, j *meta.Meta) int { iVal, iOk := i.Get(key) jVal, jOk := j.Get(key) if result := compareFound(iOk, jOk); result != 0 { return result } return cmp.Compare(meta.ExpandTimestamp(iVal), meta.ExpandTimestamp(jVal)) } } func createSortNumberFunc(key string, descending bool) sortFunc { if descending { return func(i, j *meta.Meta) int { iVal, iOk := getNum(i, key) jVal, jOk := getNum(j, key) if result := compareFound(jOk, iOk); result != 0 { return result } return cmp.Compare(jVal, iVal) } } return func(i, j *meta.Meta) int { iVal, iOk := getNum(i, key) jVal, jOk := getNum(j, key) if result := compareFound(iOk, jOk); result != 0 { return result } return cmp.Compare(iVal, jVal) } } func getNum(m *meta.Meta, key string) (int64, bool) { if s, ok := m.Get(key); ok { if i, err := strconv.ParseInt(string(s), 10, 64); err == nil { return i, true } } return 0, false } func createSortStringFunc(key string, descending bool) sortFunc { if descending { return func(i, j *meta.Meta) int { iVal, iOk := i.Get(key) jVal, jOk := j.Get(key) if result := compareFound(jOk, iOk); result != 0 { return result } return cmp.Compare(jVal, iVal) } } return func(i, j *meta.Meta) int { iVal, iOk := i.Get(key) jVal, jOk := j.Get(key) if result := compareFound(iOk, jOk); result != 0 { return result } return cmp.Compare(iVal, jVal) } } func compareFound(iOk, jOk bool) int { if iOk { if jOk { return 0 } return 1 } if jOk { return -1 } return 0 } |
Added internal/query/specs.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //----------------------------------------------------------------------------- // Copyright (c) 2023-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package query import "t73f.de/r/zsc/api" // IdentSpec contains all specification values to calculate the ident directive. type IdentSpec struct{} // Print the spec on the given print environment. func (spec *IdentSpec) Print(pe *PrintEnv) { pe.printSpace() pe.writeString(api.IdentDirective) } // ItemsSpec contains all specification values to calculate items. type ItemsSpec struct{} // Print the spec on the given print environment. func (spec *ItemsSpec) Print(pe *PrintEnv) { pe.printSpace() pe.writeString(api.ItemsDirective) } |
Added internal/query/unlinked.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | //----------------------------------------------------------------------------- // Copyright (c) 2023-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/strfun" ) // UnlinkedSpec contains all specification values to calculate unlinked references. type UnlinkedSpec struct { words []string } // Print the spec on the given print environment. func (spec *UnlinkedSpec) Print(pe *PrintEnv) { pe.printSpace() pe.writeString(api.UnlinkedDirective) for _, word := range spec.words { pe.writeStrings(" ", api.PhraseDirective, " ", word) } } // GetWords returns all title words of a given query result. func (spec *UnlinkedSpec) GetWords(metaSeq []*meta.Meta) []string { if words := spec.words; len(words) > 0 { result := make([]string, len(words)) copy(result, words) return result } result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title for _, m := range metaSeq { title, hasTitle := m.Get(meta.KeyTitle) if !hasTitle { continue } result = append(result, strfun.MakeWords(string(title))...) } return result } |
Added internal/usecase/authenticate.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "math/rand/v2" "net/http" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/auth/cred" "zettelstore.de/z/internal/logger" ) // Authenticate is the data for this use case. type Authenticate struct { log *logger.Logger token auth.TokenManager ucGetUser *GetUser } // NewAuthenticate creates a new use case. func NewAuthenticate(log *logger.Logger, token auth.TokenManager, ucGetUser *GetUser) Authenticate { return Authenticate{ log: log, token: token, ucGetUser: ucGetUser, } } // Run executes the use case. // // Parameter "r" is just included to produce better logging messages. It may be nil. Do not use it // for other purposes. func (uc *Authenticate) Run(ctx context.Context, r *http.Request, ident, credential string, d time.Duration, k auth.TokenKind) ([]byte, error) { identMeta, err := uc.ucGetUser.Run(ctx, ident) defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond) if identMeta == nil || err != nil { uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("No user with given ident found") compensateCompare() return nil, err } if hashCred, ok := identMeta.Get(meta.KeyCredential); ok { ok, err = cred.CompareHashAndCredential(string(hashCred), identMeta.Zid, ident, credential) if err != nil { uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("Error while comparing credentials") return nil, err } if ok { token, err2 := uc.token.GetToken(identMeta, d, k) if err2 != nil { uc.log.Info().Str("ident", ident).Err(err).Msg("Unable to produce authentication token") return nil, err2 } uc.log.Info().Str("user", ident).Msg("Successful") return token, nil } uc.log.Info().Str("ident", ident).HTTPIP(r).Msg("Credentials don't match") return nil, nil } uc.log.Info().Str("ident", ident).Msg("No credential stored") compensateCompare() return nil, nil } // compensateCompare if normal comapare is not possible, to avoid timing hints. func compensateCompare() { cred.CompareHashAndCredential( "$2a$10$WHcSO3G9afJ3zlOYQR1suuf83bCXED2jmzjti/MH4YH4l2mivDuze", id.Invalid, "", "") } // addDelay after credential checking to allow some CPU time for other tasks. // durDelay is the normal delay, if time spend for checking is smaller than // the minimum delay minDelay. In addition some jitter (+/- 50 ms) is added. func addDelay(start time.Time, durDelay, minDelay time.Duration) { jitter := time.Duration(rand.IntN(100)-50) * time.Millisecond if elapsed := time.Since(start); elapsed+minDelay < durDelay { time.Sleep(durDelay - elapsed + jitter) } else { time.Sleep(minDelay + jitter) } } // IsAuthenticatedPort contains method for this usecase. type IsAuthenticatedPort interface { GetUser(context.Context) *meta.Meta } // IsAuthenticated cheks if the caller is alrwady authenticated. type IsAuthenticated struct { log *logger.Logger port IsAuthenticatedPort authz auth.AuthzManager } // NewIsAuthenticated creates a new use case object. func NewIsAuthenticated(log *logger.Logger, port IsAuthenticatedPort, authz auth.AuthzManager) IsAuthenticated { return IsAuthenticated{ log: log, port: port, authz: authz, } } // IsAuthenticatedResult is an enumeration. type IsAuthenticatedResult uint8 // Values for IsAuthenticatedResult. const ( _ IsAuthenticatedResult = iota IsAuthenticatedDisabled IsAuthenticatedAndValid IsAuthenticatedAndInvalid ) // Run executes the use case. func (uc *IsAuthenticated) Run(ctx context.Context) IsAuthenticatedResult { if !uc.authz.WithAuth() { uc.log.Info().Str("auth", "disabled").Msg("IsAuthenticated") return IsAuthenticatedDisabled } if uc.port.GetUser(ctx) == nil { uc.log.Info().Msg("IsAuthenticated is false") return IsAuthenticatedAndInvalid } uc.log.Info().Msg("IsAuthenticated is true") return IsAuthenticatedAndValid } |
Added internal/usecase/create_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/zettel" ) // CreateZettelPort is the interface used by this use case. type CreateZettelPort interface { // CreateZettel creates a new zettel. CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) } // CreateZettel is the data for this use case. type CreateZettel struct { log *logger.Logger rtConfig config.Config port CreateZettelPort } // NewCreateZettel creates a new use case. func NewCreateZettel(log *logger.Logger, rtConfig config.Config, port CreateZettelPort) CreateZettel { return CreateZettel{ log: log, rtConfig: rtConfig, port: port, } } // PrepareCopy the zettel for further modification. func (*CreateZettel) PrepareCopy(origZettel zettel.Zettel) zettel.Zettel { origMeta := origZettel.Meta m := origMeta.Clone() if title, found := origMeta.Get(meta.KeyTitle); found { m.Set(meta.KeyTitle, prependTitle(title, "Copy", "Copy of ")) } setReadonly(m) content := origZettel.Content content.TrimSpace() return zettel.Zettel{Meta: m, Content: content} } // PrepareVersion the zettel for further modification. func (*CreateZettel) PrepareVersion(origZettel zettel.Zettel) zettel.Zettel { origMeta := origZettel.Meta m := origMeta.Clone() m.Set(meta.KeyPredecessor, meta.Value(origMeta.Zid.String())) setReadonly(m) content := origZettel.Content content.TrimSpace() return zettel.Zettel{Meta: m, Content: content} } // PrepareFolge the zettel for further modification. func (*CreateZettel) PrepareFolge(origZettel zettel.Zettel) zettel.Zettel { origMeta := origZettel.Meta m := meta.New(id.Invalid) if title, found := origMeta.Get(meta.KeyTitle); found { m.Set(meta.KeyTitle, prependTitle(title, "Folge", "Folge of ")) } updateMetaRoleTagsSyntax(m, origMeta) m.Set(meta.KeyPrecursor, meta.Value(origMeta.Zid.String())) return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)} } // PrepareSequel the zettel for further modification. func (*CreateZettel) PrepareSequel(origZettel zettel.Zettel) zettel.Zettel { origMeta := origZettel.Meta m := meta.New(id.Invalid) if title, found := origMeta.Get(meta.KeyTitle); found { m.Set(meta.KeyTitle, prependTitle(title, "Sequel", "Sequel of ")) } updateMetaRoleTagsSyntax(m, origMeta) m.Set(meta.KeyPrequel, meta.Value(origMeta.Zid.String())) return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)} } // PrepareNew the zettel for further modification. func (*CreateZettel) PrepareNew(origZettel zettel.Zettel, newTitle string) zettel.Zettel { m := meta.New(id.Invalid) om := origZettel.Meta m.SetNonEmpty(meta.KeyTitle, om.GetDefault(meta.KeyTitle, "")) updateMetaRoleTagsSyntax(m, om) const prefixLen = len(meta.NewPrefix) for key, val := range om.Rest() { if len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix { m.Set(key[prefixLen:], val) } } if newTitle != "" { m.Set(meta.KeyTitle, meta.Value(newTitle)) } content := origZettel.Content content.TrimSpace() return zettel.Zettel{Meta: m, Content: content} } func updateMetaRoleTagsSyntax(m, orig *meta.Meta) { m.SetNonEmpty(meta.KeyRole, orig.GetDefault(meta.KeyRole, "")) m.SetNonEmpty(meta.KeyTags, orig.GetDefault(meta.KeyTags, "")) m.SetNonEmpty(meta.KeySyntax, orig.GetDefault(meta.KeySyntax, meta.DefaultSyntax)) } func prependTitle(title, s0, s1 meta.Value) meta.Value { if len(title) > 0 { return s1 + title } return s0 } func setReadonly(m *meta.Meta) { if _, found := m.Get(meta.KeyReadOnly); found { // Currently, "false" is a safe value. // // If the current user and its role is known, a more elaborative calculation // could be done: set it to a value, so that the current user will be able // to modify it later. m.Set(meta.KeyReadOnly, meta.ValueFalse) } } // Run executes the use case. func (uc *CreateZettel) Run(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { m := zettel.Meta if m.Zid.IsValid() { return m.Zid, nil // TODO: new error: already exists } m.Set(meta.KeyCreated, meta.Value(time.Now().Local().Format(id.TimestampLayout))) m.Delete(meta.KeyModified) m.YamlSep = uc.rtConfig.GetYAMLHeader() zettel.Content.TrimSpace() zid, err := uc.port.CreateZettel(ctx, zettel) uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Create zettel") return zid, err } |
Added internal/usecase/delete_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/logger" ) // DeleteZettelPort is the interface used by this use case. type DeleteZettelPort interface { // DeleteZettel removes the zettel from the box. DeleteZettel(ctx context.Context, zid id.Zid) error } // DeleteZettel is the data for this use case. type DeleteZettel struct { log *logger.Logger port DeleteZettelPort } // NewDeleteZettel creates a new use case. func NewDeleteZettel(log *logger.Logger, port DeleteZettelPort) DeleteZettel { return DeleteZettel{log: log, port: port} } // Run executes the use case. func (uc *DeleteZettel) Run(ctx context.Context, zid id.Zid) error { err := uc.port.DeleteZettel(ctx, zid) uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Delete zettel") return err } |
Added internal/usecase/evaluate.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/evaluator" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) // Evaluate is the data for this use case. type Evaluate struct { rtConfig config.Config ucGetZettel *GetZettel ucQuery *Query } // NewEvaluate creates a new use case. func NewEvaluate(rtConfig config.Config, ucGetZettel *GetZettel, ucQuery *Query) Evaluate { return Evaluate{ rtConfig: rtConfig, ucGetZettel: ucGetZettel, ucQuery: ucQuery, } } // Run executes the use case. func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) { zettel, err := uc.ucGetZettel.Run(ctx, zid) if err != nil { return nil, err } return uc.RunZettel(ctx, zettel, syntax), nil } // RunZettel executes the use case for a given zettel. func (uc *Evaluate) RunZettel(ctx context.Context, zettel zettel.Zettel, syntax string) *ast.ZettelNode { zn := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig) evaluator.EvaluateZettel(ctx, uc, uc.rtConfig, zn) return zn } // RunBlockNode executes the use case for a metadata list. func (uc *Evaluate) RunBlockNode(ctx context.Context, bn ast.BlockNode) ast.BlockSlice { if bn == nil { return nil } bns := ast.BlockSlice{bn} evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns) return bns } // GetZettel retrieves the full zettel of a given zettel identifier. func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { return uc.ucGetZettel.Run(ctx, zid) } // QueryMeta returns a list of metadata that comply to the given selection criteria. func (uc *Evaluate) QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { return uc.ucQuery.Run(ctx, q) } |
Added internal/usecase/get_all_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/zettel" ) // GetAllZettelPort is the interface used by this use case. type GetAllZettelPort interface { GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) } // GetAllZettel is the data for this use case. type GetAllZettel struct { port GetAllZettelPort } // NewGetAllZettel creates a new use case. func NewGetAllZettel(port GetAllZettelPort) GetAllZettel { return GetAllZettel{port: port} } // Run executes the use case. func (uc GetAllZettel) Run(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { return uc.port.GetAllZettel(ctx, zid) } |
Added internal/usecase/get_special_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | //----------------------------------------------------------------------------- // Copyright (c) 2023-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) // TagZettel is the usecase of retrieving a "tag zettel", i.e. a zettel that // describes a given tag. A tag zettel must have the tag's name in its title // and must have a role=tag. // TagZettelPort is the interface used by this use case. type TagZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) } // TagZettel is the data for this use case. type TagZettel struct { port GetZettelPort query *Query } // NewTagZettel creates a new use case. func NewTagZettel(port GetZettelPort, query *Query) TagZettel { return TagZettel{port: port, query: query} } // Run executes the use case. func (uc TagZettel) Run(ctx context.Context, tag meta.Value) (zettel.Zettel, error) { tag = tag.NormalizeTag() q := query.Parse( meta.KeyTitle + api.SearchOperatorEqual + string(tag) + " " + meta.KeyRole + api.SearchOperatorHas + meta.ValueRoleTag) ml, err := uc.query.Run(ctx, q) if err != nil { return zettel.Zettel{}, err } for _, m := range ml { z, errZ := uc.port.GetZettel(ctx, m.Zid) if errZ == nil { return z, nil } } return zettel.Zettel{}, ErrTagZettelNotFound{Tag: tag} } // ErrTagZettelNotFound is returned if a tag zettel was not found. type ErrTagZettelNotFound struct{ Tag meta.Value } func (etznf ErrTagZettelNotFound) Error() string { return "tag zettel not found: " + string(etznf.Tag) } // RoleZettel is the usecase of retrieving a "role zettel", i.e. a zettel that // describes a given role. A role zettel must have the role's name in its title // and must have a role=role. // RoleZettelPort is the interface used by this use case. type RoleZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) } // RoleZettel is the data for this use case. type RoleZettel struct { port GetZettelPort query *Query } // NewRoleZettel creates a new use case. func NewRoleZettel(port GetZettelPort, query *Query) RoleZettel { return RoleZettel{port: port, query: query} } // Run executes the use case. func (uc RoleZettel) Run(ctx context.Context, role meta.Value) (zettel.Zettel, error) { q := query.Parse( meta.KeyTitle + api.SearchOperatorEqual + string(role) + " " + meta.KeyRole + api.SearchOperatorHas + meta.ValueRoleRole) ml, err := uc.query.Run(ctx, q) if err != nil { return zettel.Zettel{}, err } for _, m := range ml { z, errZ := uc.port.GetZettel(ctx, m.Zid) if errZ == nil { return z, nil } } return zettel.Zettel{}, ErrRoleZettelNotFound{Role: role} } // ErrRoleZettelNotFound is returned if a role zettel was not found. type ErrRoleZettelNotFound struct{ Role meta.Value } func (etznf ErrRoleZettelNotFound) Error() string { return "role zettel not found: " + string(etznf.Role) } |
Added internal/usecase/get_user.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) // Use case: return user identified by meta key ident. // --------------------------------------------------- // GetUserPort is the interface used by this use case. type GetUserPort interface { GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) } // GetUser is the data for this use case. type GetUser struct { authz auth.AuthzManager port GetUserPort } // NewGetUser creates a new use case. func NewGetUser(authz auth.AuthzManager, port GetUserPort) GetUser { return GetUser{authz: authz, port: port} } // Run executes the use case. func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) { ctx = box.NoEnrichContext(ctx) // It is important to try first with the owner. First, because another user // could give herself the same ''ident''. Second, in most cases the owner // will authenticate. identZettel, err := uc.port.GetZettel(ctx, uc.authz.Owner()) if err == nil && string(identZettel.Meta.GetDefault(meta.KeyUserID, "")) == ident { return identZettel.Meta, nil } // Owner was not found or has another ident. Try via list search. q := query.Parse(meta.KeyUserID + api.SearchOperatorHas + ident + " " + api.SearchOperatorHas + ident) metaList, err := uc.port.SelectMeta(ctx, nil, q) if err != nil { return nil, err } if len(metaList) < 1 { return nil, nil } return metaList[len(metaList)-1], nil } // Use case: return an user identified by zettel id and assert given ident value. // ------------------------------------------------------------------------------ // GetUserByZidPort is the interface used by this use case. type GetUserByZidPort interface { GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) } // GetUserByZid is the data for this use case. type GetUserByZid struct { port GetUserByZidPort } // NewGetUserByZid creates a new use case. func NewGetUserByZid(port GetUserByZidPort) GetUserByZid { return GetUserByZid{port: port} } // GetUser executes the use case. func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { userZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), zid) if err != nil { return nil, err } userMeta := userZettel.Meta if val, ok := userMeta.Get(meta.KeyUserID); !ok || string(val) != ident { return nil, nil } return userMeta, nil } |
Added internal/usecase/get_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/zettel" ) // GetZettelPort is the interface used by this use case. type GetZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) } // GetZettel is the data for this use case. type GetZettel struct { port GetZettelPort } // NewGetZettel creates a new use case. func NewGetZettel(port GetZettelPort) GetZettel { return GetZettel{port: port} } // Run executes the use case. func (uc GetZettel) Run(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { return uc.port.GetZettel(ctx, zid) } |
Added internal/usecase/lists.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/query" ) // -------- List syntax ------------------------------------------------------ // ListSyntaxPort is the interface used by this use case. type ListSyntaxPort interface { SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) } // ListSyntax is the data for this use case. type ListSyntax struct { port ListSyntaxPort } // NewListSyntax creates a new use case. func NewListSyntax(port ListSyntaxPort) ListSyntax { return ListSyntax{port: port} } // Run executes the use case. func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) { q := query.Parse(meta.KeySyntax + api.ExistOperator) // We look for all metadata with a syntax key metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q) if err != nil { return nil, err } result := meta.CreateArrangement(metas, meta.KeySyntax) for _, syn := range parser.GetSyntaxes() { if _, found := result[syn]; !found { delete(result, syn) } } return result, nil } // -------- List roles ------------------------------------------------------- // ListRolesPort is the interface used by this use case. type ListRolesPort interface { SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) } // ListRoles is the data for this use case. type ListRoles struct { port ListRolesPort } // NewListRoles creates a new use case. func NewListRoles(port ListRolesPort) ListRoles { return ListRoles{port: port} } // Run executes the use case. func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) { q := query.Parse(meta.KeyRole + api.ExistOperator) // We look for all metadata with an existing role key metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q) if err != nil { return nil, err } return meta.CreateArrangement(metas, meta.KeyRole), nil } |
Added internal/usecase/parse_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/parser" ) // ParseZettel is the data for this use case. type ParseZettel struct { rtConfig config.Config getZettel GetZettel } // NewParseZettel creates a new use case. func NewParseZettel(rtConfig config.Config, getZettel GetZettel) ParseZettel { return ParseZettel{rtConfig: rtConfig, getZettel: getZettel} } // Run executes the use case. func (uc ParseZettel) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) { zettel, err := uc.getZettel.Run(ctx, zid) if err != nil { return nil, err } return parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil } |
Added internal/usecase/query.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "errors" "fmt" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/collect" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" "zettelstore.de/z/strfun" ) // QueryPort is the interface used by this use case. type QueryPort interface { GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) } // Query is the data for this use case. type Query struct { port QueryPort ucEvaluate Evaluate } // NewQuery creates a new use case. func NewQuery(port QueryPort) Query { return Query{port: port} } // SetEvaluate sets the usecase Evaluate, because of circular dependencies. func (uc *Query) SetEvaluate(ucEvaluate *Evaluate) { uc.ucEvaluate = *ucEvaluate } // Run executes the use case. func (uc *Query) Run(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { zids := q.GetZids() if zids == nil { return uc.port.SelectMeta(ctx, nil, q) } if len(zids) == 0 { return nil, nil } metaSeq, err := uc.getMetaZid(ctx, zids) if err != nil { return metaSeq, err } if metaSeq = uc.processDirectives(ctx, metaSeq, q.GetDirectives()); len(metaSeq) > 0 { return uc.port.SelectMeta(ctx, metaSeq, q) } return nil, nil } func (uc *Query) getMetaZid(ctx context.Context, zids []id.Zid) ([]*meta.Meta, error) { metaSeq := make([]*meta.Meta, 0, len(zids)) for _, zid := range zids { m, err := uc.port.GetMeta(ctx, zid) if err == nil { metaSeq = append(metaSeq, m) continue } if errors.Is(err, &box.ErrNotAllowed{}) { continue } return metaSeq, err } return metaSeq, nil } func (uc *Query) processDirectives(ctx context.Context, metaSeq []*meta.Meta, directives []query.Directive) []*meta.Meta { if len(directives) == 0 { return metaSeq } for _, dir := range directives { if len(metaSeq) == 0 { return nil } switch ds := dir.(type) { case *query.ContextSpec: metaSeq = uc.processContextDirective(ctx, ds, metaSeq) case *query.IdentSpec: // Nothing to do. case *query.ItemsSpec: metaSeq = uc.processItemsDirective(ctx, ds, metaSeq) case *query.UnlinkedSpec: metaSeq = uc.processUnlinkedDirective(ctx, ds, metaSeq) default: panic(fmt.Sprintf("Unknown directive %T", ds)) } } if len(metaSeq) == 0 { return nil } return metaSeq } func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta { return spec.Execute(ctx, metaSeq, uc.port) } func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta { result := make([]*meta.Meta, 0, len(metaSeq)) for _, m := range metaSeq { zn, err := uc.ucEvaluate.Run(ctx, m.Zid, string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax))) if err != nil { continue } for _, ln := range collect.Order(zn) { ref := ln.Ref if !ref.IsZettel() { continue } if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil { if z, err3 := uc.port.GetZettel(ctx, collectedZid); err3 == nil { result = append(result, z.Meta) } } } } return result } func (uc *Query) processUnlinkedDirective(ctx context.Context, spec *query.UnlinkedSpec, metaSeq []*meta.Meta) []*meta.Meta { words := spec.GetWords(metaSeq) if len(words) == 0 { return metaSeq } var sb strings.Builder for _, word := range words { sb.WriteString(" :") sb.WriteString(word) } q := (*query.Query)(nil).Parse(sb.String()) candidates, err := uc.port.SelectMeta(ctx, nil, q) if err != nil { return nil } metaZids := idset.NewCap(len(metaSeq)) refZids := idset.NewCap(len(metaSeq) * 4) // Assumption: there are four zids per zettel for _, m := range metaSeq { metaZids.Add(m.Zid) refZids.Add(m.Zid) for key, val := range m.ComputedRest() { switch meta.Type(key) { case meta.TypeID: if zid, errParse := id.Parse(string(val)); errParse == nil { refZids.Add(zid) } case meta.TypeIDSet: for val := range val.Fields() { if zid, errParse := id.Parse(val); errParse == nil { refZids.Add(zid) } } } } } candidates = filterByZid(candidates, refZids) return uc.filterCandidates(ctx, candidates, words) } func filterByZid(candidates []*meta.Meta, ignoreSeq *idset.Set) []*meta.Meta { result := make([]*meta.Meta, 0, len(candidates)) for _, m := range candidates { if !ignoreSeq.Contains(m.Zid) { result = append(result, m) } } return result } func (uc *Query) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta { result := make([]*meta.Meta, 0, len(candidates)) for _, cand := range candidates { zettel, err := uc.port.GetZettel(ctx, cand.Zid) if err != nil { continue } v := unlinkedVisitor{ words: words, found: false, } v.text = v.joinWords(words) syntax := string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)) if !parser.IsASTParser(syntax) { continue } zn := uc.ucEvaluate.RunZettel(ctx, zettel, syntax) ast.Walk(&v, &zn.BlocksAST) if v.found { result = append(result, cand) } } return result } func (*unlinkedVisitor) joinWords(words []string) string { return " " + strings.ToLower(strings.Join(words, " ")) + " " } type unlinkedVisitor struct { words []string text string found bool } func (v *unlinkedVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.InlineSlice: v.checkWords(n) return nil case *ast.HeadingNode: return nil case *ast.LinkNode, *ast.EmbedRefNode, *ast.EmbedBLOBNode, *ast.CiteNode: return nil } return v } func (v *unlinkedVisitor) checkWords(is *ast.InlineSlice) { if len(*is) < 2*len(v.words)-1 { return } for _, text := range v.splitInlineTextList(is) { if strings.Contains(text, v.text) { v.found = true } } } func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string { var result []string var curList []string for _, in := range *is { switch n := in.(type) { case *ast.TextNode: curList = append(curList, strfun.MakeWords(n.Text)...) default: if curList != nil { result = append(result, v.joinWords(curList)) curList = nil } } } if curList != nil { result = append(result, v.joinWords(curList)) } return result } |
Added internal/usecase/refresh.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/internal/logger" ) // RefreshPort is the interface used by this use case. type RefreshPort interface { Refresh(context.Context) error } // Refresh is the data for this use case. type Refresh struct { log *logger.Logger port RefreshPort } // NewRefresh creates a new use case. func NewRefresh(log *logger.Logger, port RefreshPort) Refresh { return Refresh{log: log, port: port} } // Run executes the use case. func (uc *Refresh) Run(ctx context.Context) error { err := uc.port.Refresh(ctx) uc.log.Info().User(ctx).Err(err).Msg("Refresh internal data") return err } |
Added internal/usecase/reindex.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | //----------------------------------------------------------------------------- // Copyright (c) 2023-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/logger" ) // ReIndexPort is the interface used by this use case. type ReIndexPort interface { ReIndex(context.Context, id.Zid) error } // ReIndex is the data for this use case. type ReIndex struct { log *logger.Logger port ReIndexPort } // NewReIndex creates a new use case. func NewReIndex(log *logger.Logger, port ReIndexPort) ReIndex { return ReIndex{log: log, port: port} } // Run executes the use case. func (uc *ReIndex) Run(ctx context.Context, zid id.Zid) error { err := uc.port.ReIndex(ctx, zid) uc.log.Info().User(ctx).Err(err).Zid(zid).Msg("ReIndex zettel") return err } |
Added internal/usecase/update_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/zettel" ) // UpdateZettelPort is the interface used by this use case. type UpdateZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) // UpdateZettel updates an existing zettel. UpdateZettel(ctx context.Context, zettel zettel.Zettel) error } // UpdateZettel is the data for this use case. type UpdateZettel struct { log *logger.Logger port UpdateZettelPort } // NewUpdateZettel creates a new use case. func NewUpdateZettel(log *logger.Logger, port UpdateZettelPort) UpdateZettel { return UpdateZettel{log: log, port: port} } // Run executes the use case. func (uc *UpdateZettel) Run(ctx context.Context, zettel zettel.Zettel, hasContent bool) error { m := zettel.Meta oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid) if err != nil { return err } if zettel.Equal(oldZettel, false) { return nil } // Update relevant computed, but stored values. if _, found := m.Get(meta.KeyCreated); !found { if val, crFound := oldZettel.Meta.Get(meta.KeyCreated); crFound { m.Set(meta.KeyCreated, val) } } m.SetNow(meta.KeyModified) m.YamlSep = oldZettel.Meta.YamlSep if m.Zid == id.ZidConfiguration { m.Set(meta.KeySyntax, meta.ValueSyntaxNone) } if !hasContent { zettel.Content = oldZettel.Content } zettel.Content.TrimSpace() err = uc.port.UpdateZettel(ctx, zettel) uc.log.Info().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel") return err } |
Added internal/usecase/usecase.go.
> > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase |
Added internal/usecase/version.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "regexp" "strconv" "zettelstore.de/z/internal/kernel" ) // Version is the data for this use case. type Version struct { vr VersionResult } // NewVersion creates a new use case. func NewVersion(version string) Version { return Version{calculateVersionResult(version)} } // VersionResult is the data structure returned by this usecase. type VersionResult struct { Major int Minor int Patch int Info string Hash string } var invalidVersion = VersionResult{ Major: -1, Minor: -1, Patch: -1, Info: kernel.CoreDefaultVersion, Hash: "", } var reVersion = regexp.MustCompile(`^(\d+)\.(\d+)(\.(\d+))?(-(([[:alnum:]]|-)+))?(\+(([[:alnum:]])+(-[[:alnum:]]+)?))?`) func calculateVersionResult(version string) VersionResult { match := reVersion.FindStringSubmatch(version) if len(match) < 12 { return invalidVersion } major, err := strconv.Atoi(match[1]) if err != nil { return invalidVersion } minor, err := strconv.Atoi(match[2]) if err != nil { return invalidVersion } patch, err := strconv.Atoi(match[4]) if err != nil { patch = 0 } return VersionResult{ Major: major, Minor: minor, Patch: patch, Info: match[6], Hash: match[9], } } // Run executes the use case. func (uc Version) Run() VersionResult { return uc.vr } |
Added internal/web/adapter/adapter.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | //----------------------------------------------------------------------------- // Copyright (c) 2024-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2024-present Detlef Stern //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests, and some helper tools. package adapter import ( "context" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/usecase" ) // TryReIndex executes a re-index if the appropriate query action is given. func TryReIndex(ctx context.Context, actions []string, metaSeq []*meta.Meta, reIndex *usecase.ReIndex) ([]string, error) { if lenActions := len(actions); lenActions > 0 { tempActions := make([]string, 0, lenActions) hasReIndex := false for _, act := range actions { if !hasReIndex && act == api.ReIndexAction { hasReIndex = true var errAction error for _, m := range metaSeq { if err := reIndex.Run(ctx, m.Zid); err != nil { errAction = err } } if errAction != nil { return nil, errAction } continue } tempActions = append(tempActions, act) } return tempActions, nil } return nil, nil } |
Added internal/web/adapter/api/api.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "bytes" "context" "net/http" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/web/server" ) // API holds all data and methods for delivering API call results. type API struct { log *logger.Logger b server.Builder authz auth.AuthzManager token auth.TokenManager rtConfig config.Config policy auth.Policy tokenLifetime time.Duration } // New creates a new API object. func New(log *logger.Logger, b server.Builder, authz auth.AuthzManager, token auth.TokenManager, rtConfig config.Config, pol auth.Policy) *API { a := &API{ log: log, b: b, authz: authz, token: token, rtConfig: rtConfig, policy: pol, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration), } return a } // NewURLBuilder creates a new URL builder object with the given key. func (a *API) NewURLBuilder(key byte) *api.URLBuilder { return a.b.NewURLBuilder(key) } func (a *API) getAuthData(ctx context.Context) *server.AuthData { return server.GetAuthData(ctx) } func (a *API) withAuth() bool { return a.authz.WithAuth() } func (a *API) getToken(ident *meta.Meta) ([]byte, error) { return a.token.GetToken(ident, a.tokenLifetime, auth.KindAPI) } func (a *API) reportUsecaseError(w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { a.log.Error().Err(err).Msg(text) http.Error(w, http.StatusText(code), code) return } // TODO: must call PrepareHeader somehow http.Error(w, text, code) } func writeBuffer(w http.ResponseWriter, buf *bytes.Buffer, contentType string) error { return adapter.WriteData(w, buf.Bytes(), contentType) } func (a *API) getRights(ctx context.Context, m *meta.Meta) (result api.ZettelRights) { pol := a.policy user := server.GetUser(ctx) if pol.CanCreate(user, m) { result |= api.ZettelCanCreate } if pol.CanRead(user, m) { result |= api.ZettelCanRead } if pol.CanWrite(user, m, m) { result |= api.ZettelCanWrite } if pol.CanDelete(user, m) { result |= api.ZettelCanDelete } if result == 0 { return api.ZettelCanNone } return result } |
Added internal/web/adapter/api/command.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package api import ( "context" "net/http" "t73f.de/r/zsc/api" "zettelstore.de/z/internal/usecase" ) // MakePostCommandHandler creates a new HTTP handler to execute certain commands. func (a *API) MakePostCommandHandler( ucIsAuth *usecase.IsAuthenticated, ucRefresh *usecase.Refresh, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() switch api.Command(r.URL.Query().Get(api.QueryKeyCommand)) { case api.CommandAuthenticated: handleIsAuthenticated(ctx, w, ucIsAuth) return case api.CommandRefresh: err := ucRefresh.Run(ctx) if err != nil { a.reportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) return } http.Error(w, "Unknown command", http.StatusBadRequest) }) } func handleIsAuthenticated(ctx context.Context, w http.ResponseWriter, ucIsAuth *usecase.IsAuthenticated) { switch ucIsAuth.Run(ctx) { case usecase.IsAuthenticatedDisabled: w.WriteHeader(http.StatusOK) case usecase.IsAuthenticatedAndValid: w.WriteHeader(http.StatusNoContent) case usecase.IsAuthenticatedAndInvalid: w.WriteHeader(http.StatusUnauthorized) default: http.Error(w, "Unexpected result value", http.StatusInternalServerError) } } |
Added internal/web/adapter/api/create_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package api import ( "net/http" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/web/content" "zettelstore.de/z/internal/zettel" ) // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() enc, encStr := getEncoding(r, q) var zettel zettel.Zettel var err error switch enc { case api.EncoderPlain: zettel, err = buildZettelFromPlainData(r, id.Invalid) case api.EncoderData: zettel, err = buildZettelFromData(r, id.Invalid) default: http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if err != nil { a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } ctx := r.Context() newZid, err := createZettel.Run(ctx, zettel) if err != nil { a.reportUsecaseError(w, err) return } var result []byte var contentType string location := a.NewURLBuilder('z').SetZid(newZid) switch enc { case api.EncoderPlain: result = newZid.Bytes() contentType = content.PlainText case api.EncoderData: result = []byte(sx.Int64(newZid).String()) contentType = content.SXPF default: panic(encStr) } h := adapter.PrepareHeader(w, contentType) h.Set(api.HeaderLocation, location.String()) w.WriteHeader(http.StatusCreated) if _, err = w.Write(result); err != nil { a.log.Error().Err(err).Zid(newZid).Msg("Create Zettel") } }) } |
Added internal/web/adapter/api/delete_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package api import ( "net/http" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/usecase" ) // MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } if err = deleteZettel.Run(r.Context(), zid); err != nil { a.reportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) }) } |
Added internal/web/adapter/api/get_data.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package api import ( "net/http" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/usecase" ) // MakeGetDataHandler creates a new HTTP handler to return zettelstore data. func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { version := ucVersion.Run() err := a.writeObject(w, id.Invalid, sx.MakeList( sx.Int64(version.Major), sx.Int64(version.Minor), sx.Int64(version.Patch), sx.MakeString(version.Info), sx.MakeString(version.Hash), )) if err != nil { a.log.Error().Err(err).Msg("Write Version Info") } }) } |
Added internal/web/adapter/api/get_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package api import ( "bytes" "context" "fmt" "net/http" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sexp" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/web/content" ) // MakeGetZettelHandler creates a new HTTP handler to return a zettel in various encodings. func (a *API) MakeGetZettelHandler( getZettel usecase.GetZettel, parseZettel usecase.ParseZettel, evaluate usecase.Evaluate, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() part := getPart(q, partContent) ctx := r.Context() switch enc, encStr := getEncoding(r, q); enc { case api.EncoderPlain: a.writePlainData(ctx, w, zid, part, getZettel) case api.EncoderData: a.writeSzData(ctx, w, zid, part, getZettel) default: var zn *ast.ZettelNode if q.Has(api.QueryKeyParseOnly) { zn, err = parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax)) } else { zn, err = evaluate.Run(ctx, zid, q.Get(meta.KeySyntax)) } if err != nil { a.reportUsecaseError(w, err) return } a.writeEncodedZettelPart(ctx, w, zn, enc, encStr, part) } }) } func (a *API) writePlainData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) { var buf bytes.Buffer var contentType string var err error z, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { a.reportUsecaseError(w, err) return } switch part { case partZettel: _, err = z.Meta.Write(&buf) if err == nil { err = buf.WriteByte('\n') } if err == nil { _, err = z.Content.Write(&buf) } case partMeta: contentType = content.PlainText _, err = z.Meta.Write(&buf) case partContent: contentType = content.MIMEFromSyntax(string(z.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax))) _, err = z.Content.Write(&buf) } if err != nil { a.log.Error().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if err = writeBuffer(w, &buf, contentType); err != nil { a.log.Error().Err(err).Zid(zid).Msg("Write Plain data") } } func (a *API) writeSzData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) { z, err := getZettel.Run(ctx, zid) if err != nil { a.reportUsecaseError(w, err) return } var obj sx.Object switch part { case partZettel: zContent, zEncoding := z.Content.Encode() obj = sexp.EncodeZettel(api.ZettelData{ Meta: z.Meta.Map(), Rights: a.getRights(ctx, z.Meta), Encoding: zEncoding, Content: zContent, }) case partMeta: obj = sexp.EncodeMetaRights(api.MetaRights{ Meta: z.Meta.Map(), Rights: a.getRights(ctx, z.Meta), }) } if err = a.writeObject(w, zid, obj); err != nil { a.log.Error().Err(err).Zid(zid).Msg("write sx data") } } func (a *API) writeEncodedZettelPart( ctx context.Context, w http.ResponseWriter, zn *ast.ZettelNode, enc api.EncodingEnum, encStr string, part partType, ) { encdr := encoder.Create( enc, &encoder.CreateParameter{ Lang: a.rtConfig.Get(ctx, zn.InhMeta, meta.KeyLang), }) if encdr == nil { adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid, encStr)) return } var err error var buf bytes.Buffer switch part { case partZettel: _, err = encdr.WriteZettel(&buf, zn) case partMeta: _, err = encdr.WriteMeta(&buf, zn.InhMeta) case partContent: _, err = encdr.WriteBlocks(&buf, &zn.BlocksAST) } if err != nil { a.log.Error().Err(err).Zid(zn.Zid).Msg("Unable to store data in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if buf.Len() == 0 { w.WriteHeader(http.StatusNoContent) return } if err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc)); err != nil { a.log.Error().Err(err).Zid(zn.Zid).Msg("Write Encoded Zettel") } } |
Added internal/web/adapter/api/login.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package api import ( "net/http" "time" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" ) // MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !a.withAuth() { if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil { a.log.Error().Err(err).Msg("Login/free") } return } var token []byte if ident, cred := retrieveIdentCred(r); ident != "" { var err error token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindAPI) if err != nil { a.reportUsecaseError(w, err) return } } if len(token) == 0 { w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } if err := a.writeToken(w, string(token), a.tokenLifetime); err != nil { a.log.Error().Err(err).Msg("Login") } }) } func retrieveIdentCred(r *http.Request) (string, string) { if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok { return ident, cred } if ident, cred, ok := r.BasicAuth(); ok { return ident, cred } return "", "" } // MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user. func (a *API) MakeRenewAuthHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if !a.withAuth() { if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil { a.log.Error().Err(err).Msg("Refresh/free") } return } authData := a.getAuthData(ctx) if authData == nil || len(authData.Token) == 0 || authData.User == nil { adapter.BadRequest(w, "Not authenticated") return } totalLifetime := authData.Expires.Sub(authData.Issued) currentLifetime := authData.Now.Sub(authData.Issued) // If we are in the first quarter of the tokens lifetime, return the token if currentLifetime*4 < totalLifetime { if err := a.writeToken(w, string(authData.Token), totalLifetime-currentLifetime); err != nil { a.log.Error().Err(err).Msg("Write old token") } return } // Token is a little bit aged. Create a new one token, err := a.getToken(authData.User) if err != nil { a.reportUsecaseError(w, err) return } if err = a.writeToken(w, string(token), a.tokenLifetime); err != nil { a.log.Error().Err(err).Msg("Write renewed token") } }) } func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error { return a.writeObject(w, id.Invalid, sx.MakeList( sx.MakeString("Bearer"), sx.MakeString(token), sx.Int64(int64(lifetime/time.Second)), )) } |
Added internal/web/adapter/api/query.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package api import ( "bytes" "fmt" "io" "net/http" "net/url" "strconv" "strings" "slices" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sexp" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/web/content" ) // MakeQueryHandler creates a new HTTP handler to perform a query. func (a *API) MakeQueryHandler( queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() urlQuery := r.URL.Query() if a.handleTagZettel(w, r, tagZettel, urlQuery) || a.handleRoleZettel(w, r, roleZettel, urlQuery) { return } sq := adapter.GetQuery(urlQuery) metaSeq, err := queryMeta.Run(ctx, sq) if err != nil { a.reportUsecaseError(w, err) return } actions, err := adapter.TryReIndex(ctx, sq.Actions(), metaSeq, reIndex) if err != nil { a.reportUsecaseError(w, err) return } if len(actions) > 0 { if len(metaSeq) > 0 { if slices.Contains(actions, api.RedirectAction) { zid := metaSeq[0].Zid ub := a.NewURLBuilder('z').SetZid(zid) a.redirectFound(w, r, ub, zid) return } } } var encoder zettelEncoder var contentType string switch enc, _ := getEncoding(r, urlQuery); enc { case api.EncoderPlain: encoder = &plainZettelEncoder{} contentType = content.PlainText case api.EncoderData: encoder = &dataZettelEncoder{ sq: sq, getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) }, } contentType = content.SXPF default: http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } var buf bytes.Buffer err = queryAction(&buf, encoder, metaSeq, actions) if err != nil { a.log.Error().Err(err).Str("query", sq.String()).Msg("execute query action") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } if err = writeBuffer(w, &buf, contentType); err != nil { a.log.Error().Err(err).Msg("write result buffer") } }) } func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, actions []string) error { minVal, maxVal := -1, -1 if len(actions) > 0 { acts := make([]string, 0, len(actions)) for _, act := range actions { if strings.HasPrefix(act, api.MinAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { minVal = num continue } } if strings.HasPrefix(act, api.MaxAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { maxVal = num continue } } acts = append(acts, act) } for _, act := range acts { if act == api.KeysAction { return encodeKeysArrangement(w, enc, ml, act) } switch key := strings.ToLower(act); meta.Type(key) { case meta.TypeWord, meta.TypeTagSet: return encodeMetaKeyArrangement(w, enc, ml, key, minVal, maxVal) } } } return enc.writeMetaList(w, ml) } func encodeKeysArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, act string) error { arr := make(meta.Arrangement, 128) for _, m := range ml { for k := range m.Map() { arr[k] = append(arr[k], m) } } return enc.writeArrangement(w, act, arr) } func encodeMetaKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, minVal, maxVal int) error { arr0 := meta.CreateArrangement(ml, key) arr := make(meta.Arrangement, len(arr0)) for k0, ml0 := range arr0 { if len(ml0) < minVal || (maxVal > 0 && len(ml0) > maxVal) { continue } arr[k0] = ml0 } return enc.writeArrangement(w, key, arr) } type zettelEncoder interface { writeMetaList(w io.Writer, ml []*meta.Meta) error writeArrangement(w io.Writer, act string, arr meta.Arrangement) error } type plainZettelEncoder struct{} func (*plainZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error { for _, m := range ml { _, err := fmt.Fprintln(w, m.Zid.String(), m.GetTitle()) if err != nil { return err } } return nil } func (*plainZettelEncoder) writeArrangement(w io.Writer, _ string, arr meta.Arrangement) error { for key, ml := range arr { _, err := io.WriteString(w, string(key)) if err != nil { return err } for i, m := range ml { if i == 0 { _, err = io.WriteString(w, "\t") } else { _, err = io.WriteString(w, " ") } if err != nil { return err } _, err = io.WriteString(w, m.Zid.String()) if err != nil { return err } } _, err = io.WriteString(w, "\n") if err != nil { return err } } return nil } type dataZettelEncoder struct { sq *query.Query getRights func(*meta.Meta) api.ZettelRights } func (dze *dataZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error { var lb sx.ListBuilder lb.AddN( sx.MakeSymbol("meta-list"), sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())), sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())), ) symID, symZettel := sx.MakeSymbol("id"), sx.MakeSymbol("zettel") for _, m := range ml { msz := sexp.EncodeMetaRights(api.MetaRights{ Meta: m.Map(), Rights: dze.getRights(m), }) msz = sx.Cons(sx.MakeList(symID, sx.Int64(m.Zid)), msz.Cdr()).Cons(symZettel) lb.Add(msz) } _, err := sx.Print(w, lb.List()) return err } func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error { var lb sx.ListBuilder lb.AddN( sx.MakeSymbol("meta-list"), sx.MakeString(act), sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())), sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())), ) for aggKey, metaList := range arr { var lbMeta sx.ListBuilder lbMeta.Add(sx.MakeString(aggKey)) for _, m := range metaList { lbMeta.Add(sx.Int64(m.Zid)) } lb.Add(lbMeta.List()) } _, err := sx.Print(w, lb.List()) return err } func (a *API) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool { tag := vals.Get(api.QueryKeyTag) if tag == "" { return false } ctx := r.Context() z, err := tagZettel.Run(ctx, meta.Value(tag)) if err != nil { a.reportUsecaseError(w, err) return true } zid := z.Meta.Zid newURL := a.NewURLBuilder('z').SetZid(zid) for key, slVals := range vals { if key == api.QueryKeyTag { continue } for _, val := range slVals { newURL.AppendKVQuery(key, val) } } a.redirectFound(w, r, newURL, zid) return true } func (a *API) handleRoleZettel(w http.ResponseWriter, r *http.Request, roleZettel *usecase.RoleZettel, vals url.Values) bool { role := vals.Get(api.QueryKeyRole) if role == "" { return false } ctx := r.Context() z, err := roleZettel.Run(ctx, meta.Value(role)) if err != nil { a.reportUsecaseError(w, err) return true } zid := z.Meta.Zid newURL := a.NewURLBuilder('z').SetZid(zid) for key, slVals := range vals { if key == api.QueryKeyRole { continue } for _, val := range slVals { newURL.AppendKVQuery(key, val) } } a.redirectFound(w, r, newURL, zid) return true } func (a *API) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder, zid id.Zid) { w.Header().Set(api.HeaderContentType, content.PlainText) http.Redirect(w, r, ub.String(), http.StatusFound) if _, err := io.WriteString(w, zid.String()); err != nil { a.log.Error().Err(err).Msg("redirect body") } } |
Added internal/web/adapter/api/request.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package api import ( "io" "net/http" "net/url" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sexp" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/zettel" ) // getEncoding returns the data encoding selected by the caller. func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) { encoding := q.Get(api.QueryKeyEncoding) if encoding != "" { return api.Encoder(encoding), encoding } if enc, ok := getOneEncoding(r, api.HeaderAccept); ok { return api.Encoder(enc), enc } if enc, ok := getOneEncoding(r, api.HeaderContentType); ok { return api.Encoder(enc), enc } return api.EncoderPlain, api.EncoderPlain.String() } func getOneEncoding(r *http.Request, key string) (string, bool) { if values, ok := r.Header[key]; ok { for _, value := range values { if enc, ok2 := contentType2encoding(value); ok2 { return enc, true } } } return "", false } var mapCT2encoding = map[string]string{ "text/html": api.EncodingHTML, } func contentType2encoding(contentType string) (string, bool) { // TODO: only check before first ';' enc, ok := mapCT2encoding[contentType] return enc, ok } type partType int const ( _ partType = iota partMeta partContent partZettel ) var partMap = map[string]partType{ api.PartMeta: partMeta, api.PartContent: partContent, api.PartZettel: partZettel, } func getPart(q url.Values, defPart partType) partType { if part, ok := partMap[q.Get(api.QueryKeyPart)]; ok { return part } return defPart } func (p partType) String() string { switch p { case partMeta: return "meta" case partContent: return "content" case partZettel: return "zettel" } return "" } func (p partType) DefString(defPart partType) string { if p == defPart { return "" } return p.String() } func buildZettelFromPlainData(r *http.Request, zid id.Zid) (zettel.Zettel, error) { defer r.Body.Close() b, err := io.ReadAll(r.Body) if err != nil { return zettel.Zettel{}, err } inp := input.NewInput(b) m := meta.NewFromInput(zid, inp) return zettel.Zettel{ Meta: m, Content: zettel.NewContent(inp.Src[inp.Pos:]), }, nil } func buildZettelFromData(r *http.Request, zid id.Zid) (zettel.Zettel, error) { defer r.Body.Close() rdr := sxreader.MakeReader(r.Body) obj, err := rdr.Read() if err != nil { return zettel.Zettel{}, err } zd, err := sexp.ParseZettel(obj) if err != nil { return zettel.Zettel{}, err } m := meta.New(zid) for k, v := range zd.Meta { if !meta.IsComputed(k) { m.Set(meta.RemoveNonGraphic(k), meta.Value(meta.RemoveNonGraphic(v))) } } var content zettel.Content if err = content.SetDecoded(zd.Content, zd.Encoding); err != nil { return zettel.Zettel{}, err } return zettel.Zettel{ Meta: m, Content: content, }, nil } |
Added internal/web/adapter/api/response.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package api import ( "bytes" "net/http" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/web/content" ) func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error { var buf bytes.Buffer if _, err := sx.Print(&buf, obj); err != nil { msg := a.log.Error().Err(err) if msg != nil { if zid.IsValid() { msg = msg.Zid(zid) } msg.Msg("Unable to store object in buffer") } http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return nil } return writeBuffer(w, &buf, content.SXPF) } |
Added internal/web/adapter/api/update_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package api import ( "net/http" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/zettel" ) // MakeUpdateZettelHandler creates a new HTTP handler to update a zettel. func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() var zettel zettel.Zettel switch enc, _ := getEncoding(r, q); enc { case api.EncoderPlain: zettel, err = buildZettelFromPlainData(r, zid) case api.EncoderData: zettel, err = buildZettelFromData(r, zid) default: http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if err != nil { a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } if err = updateZettel.Run(r.Context(), zettel, true); err != nil { a.reportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) }) } |
Added internal/web/adapter/errors.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package adapter import "net/http" // BadRequest signals HTTP status code 400. func BadRequest(w http.ResponseWriter, text string) { http.Error(w, text, http.StatusBadRequest) } // ErrResourceNotFound is signalled when a web resource was not found. type ErrResourceNotFound struct{ Path string } func (ernf ErrResourceNotFound) Error() string { return "resource not found: " + ernf.Path } |
Added internal/web/adapter/request.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package adapter import ( "net/http" "net/url" "strconv" "strings" "t73f.de/r/zsc/api" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/query" ) // GetCredentialsViaForm retrieves the authentication credentions from a form. func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { err := r.ParseForm() if err != nil { kernel.Main.GetLogger(kernel.WebService).Info().Err(err).Msg("Unable to parse form") return "", "", false } ident = strings.TrimSpace(r.PostFormValue("username")) cred = r.PostFormValue("password") if ident == "" { return "", "", false } return ident, cred, true } // GetQuery retrieves the specified options from a query. func GetQuery(vals url.Values) (result *query.Query) { if exprs, found := vals[api.QueryKeyQuery]; found { result = query.Parse(strings.Join(exprs, " ")) } if seeds, found := vals[api.QueryKeySeed]; found { for _, seed := range seeds { if si, err := strconv.ParseInt(seed, 10, 31); err == nil { result = result.SetSeed(int(si)) break } } } return result } |
Added internal/web/adapter/response.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package adapter import ( "errors" "fmt" "net/http" "strings" "t73f.de/r/zsc/api" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/usecase" ) // WriteData emits the given data to the response writer. func WriteData(w http.ResponseWriter, data []byte, contentType string) error { if len(data) == 0 { w.WriteHeader(http.StatusNoContent) return nil } PrepareHeader(w, contentType) w.WriteHeader(http.StatusOK) _, err := w.Write(data) return err } // PrepareHeader sets the HTTP header to defined values. func PrepareHeader(w http.ResponseWriter, contentType string) http.Header { h := w.Header() if contentType != "" { h.Set(api.HeaderContentType, contentType) } return h } // ErrBadRequest is returned if the caller made an invalid HTTP request. type ErrBadRequest struct { Text string } // NewErrBadRequest creates an new bad request error. func NewErrBadRequest(text string) error { return ErrBadRequest{Text: text} } func (err ErrBadRequest) Error() string { return err.Text } // CodeMessageFromError returns an appropriate HTTP status code and text from a given error. func CodeMessageFromError(err error) (int, string) { var eznf box.ErrZettelNotFound if errors.As(err, &eznf) { return http.StatusNotFound, "Zettel not found: " + eznf.Zid.String() } var ena *box.ErrNotAllowed if errors.As(err, &ena) { msg := ena.Error() return http.StatusForbidden, strings.ToUpper(msg[:1]) + msg[1:] } var eiz box.ErrInvalidZid if errors.As(err, &eiz) { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", eiz.Zid) } var etznf usecase.ErrTagZettelNotFound if errors.As(err, &etznf) { return http.StatusNotFound, "Tag zettel not found: " + string(etznf.Tag) } var erznf usecase.ErrRoleZettelNotFound if errors.As(err, &erznf) { return http.StatusNotFound, "Role zettel not found: " + string(erznf.Role) } var ebr ErrBadRequest if errors.As(err, &ebr) { return http.StatusBadRequest, ebr.Text } if errors.Is(err, box.ErrStopped) { return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err) } if errors.Is(err, box.ErrConflict) { return http.StatusConflict, "Zettelstore operations conflicted" } if errors.Is(err, box.ErrCapacity) { return http.StatusInsufficientStorage, "Zettelstore reached one of its storage limits" } var ernf ErrResourceNotFound if errors.As(err, &ernf) { return http.StatusNotFound, "Resource not found: " + ernf.Path } return http.StatusInternalServerError, err.Error() } |
Added internal/web/adapter/webui/const.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package webui // WebUI related constants. const queryKeyAction = "_action" // Values for queryKeyAction const ( valueActionCopy = "copy" valueActionFolge = "folge" valueActionNew = "new" valueActionSequel = "sequel" valueActionVersion = "version" ) // Enumeration for queryKeyAction type createAction uint8 const ( actionCopy createAction = iota actionFolge actionNew actionSequel actionVersion ) var createActionMap = map[string]createAction{ valueActionSequel: actionSequel, valueActionCopy: actionCopy, valueActionFolge: actionFolge, valueActionNew: actionNew, valueActionVersion: actionVersion, } func getCreateAction(s string) createAction { if action, found := createActionMap[s]; found { return action } return actionCopy } |
Added internal/web/adapter/webui/create_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "bytes" "context" "net/http" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/evaluator" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/web/server" "zettelstore.de/z/internal/zettel" ) // MakeGetCreateZettelHandler creates a new HTTP handler to display the // HTML edit view for the various zettel creation methods. func (wui *WebUI) MakeGetCreateZettelHandler( getZettel usecase.GetZettel, createZettel *usecase.CreateZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() op := getCreateAction(q.Get(queryKeyAction)) path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) return } origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { wui.reportError(ctx, w, box.ErrZettelNotFound{Zid: zid}) return } roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) switch op { case actionCopy: wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData) case actionFolge: wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData) case actionNew: title := ast.NormalizedSpacedText(origZettel.Meta.GetTitle()) newTitle := ast.NormalizedSpacedText(q.Get(meta.KeyTitle)) wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData) case actionSequel: wui.renderZettelForm(ctx, w, createZettel.PrepareSequel(origZettel), "Sequel Zettel", "", roleData, syntaxData) case actionVersion: wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData) } }) } func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) { roleData := dataListFromArrangement(ucListRoles.Run(ctx)) syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx)) return roleData, syntaxData } func dataListFromArrangement(ar meta.Arrangement, err error) []string { if err == nil { l := ar.Counted() l.SortByCount() return l.Categories() } return nil } func (wui *WebUI) renderZettelForm( ctx context.Context, w http.ResponseWriter, ztl zettel.Zettel, title string, formActionURL string, roleData []string, syntaxData []string, ) { user := server.GetUser(ctx) m := ztl.Meta var sb strings.Builder for key, val := range m.Rest() { sb.WriteString(key) sb.WriteString(": ") sb.WriteString(string(val)) sb.WriteByte('\n') } env, rb := wui.createRenderEnv(ctx, "form", wui.getUserLang(ctx), title, user) rb.bindString("heading", sx.MakeString(title)) rb.bindString("form-action-url", sx.MakeString(formActionURL)) rb.bindString("role-data", makeStringList(roleData)) rb.bindString("syntax-data", makeStringList(syntaxData)) rb.bindString("meta", sx.MakeString(sb.String())) if !ztl.Content.IsBinary() { rb.bindString("content", sx.MakeString(ztl.Content.AsString())) } wui.bindCommonZettelData(ctx, &rb, user, m, &ztl.Content) if rb.err == nil { rb.err = wui.renderSxnTemplate(ctx, w, id.ZidFormTemplate, env) } if err := rb.err; err != nil { wui.reportError(ctx, w, err) } } // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reEdit, zettel, err := parseZettelForm(r, id.Invalid) if err == errMissingContent { wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing")) return } if err != nil { const msg = "Unable to read form data" wui.log.Info().Err(err).Msg(msg) wui.reportError(ctx, w, adapter.NewErrBadRequest(msg)) return } newZid, err := createZettel.Run(ctx, zettel) if err != nil { wui.reportError(ctx, w, err) return } if reEdit { wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(newZid)) } else { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid)) } }) } // MakeGetZettelFromListHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakeGetZettelFromListHandler( queryMeta *usecase.Query, evaluate *usecase.Evaluate, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { q := adapter.GetQuery(r.URL.Query()) ctx := r.Context() metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q) if err != nil { wui.reportError(ctx, w, err) return } entries, _ := evaluator.QueryAction(ctx, q, metaSeq) bns := evaluate.RunBlockNode(ctx, entries) enc := encoder.Create(api.EncoderZmk, nil) var zmkContent bytes.Buffer _, err = enc.WriteBlocks(&zmkContent, &bns) if err != nil { wui.reportError(ctx, w, err) return } m := meta.New(id.Invalid) m.Set(meta.KeyTitle, meta.Value(q.Human())) m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) if qval := q.String(); qval != "" { m.Set(meta.KeyQuery, meta.Value(qval)) } zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(zmkContent.Bytes())} roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) wui.renderZettelForm(ctx, w, zettel, "Zettel from list", wui.createNewURL, roleData, syntaxData) }) } |
Added internal/web/adapter/webui/delete_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "net/http" "slices" "t73f.de/r/sx" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/server" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. func (wui *WebUI) MakeGetDeleteZettelHandler( getZettel usecase.GetZettel, getAllZettel usecase.GetAllZettel, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) return } zs, err := getAllZettel.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } m := zs[0].Meta user := server.GetUser(ctx) env, rb := wui.createRenderEnv( ctx, "delete", wui.getUserLang(ctx), "Delete Zettel "+m.Zid.String(), user) if len(zs) > 1 { rb.bindString("shadowed-box", sx.MakeString(string(zs[1].Meta.GetDefault(meta.KeyBoxNumber, "???")))) rb.bindString("incoming", nil) } else { rb.bindString("shadowed-box", nil) rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel))) } wui.bindCommonZettelData(ctx, &rb, user, m, nil) if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.ZidDeleteTemplate, env) } else { err = rb.err } if err != nil { wui.reportError(ctx, w, err) } }) } func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) *sx.Pair { zidMap := set.New[string]() addListValues(zidMap, m, meta.KeyBackward) for _, kd := range meta.GetSortedKeyDescriptions() { inverseKey := kd.Inverse if inverseKey == "" { continue } ikd := meta.GetDescription(inverseKey) switch ikd.Type { case meta.TypeID: if val, ok := m.Get(inverseKey); ok { zidMap.Add(string(val)) } case meta.TypeIDSet: addListValues(zidMap, m, inverseKey) } } return wui.zidLinksSxn(slices.Sorted(zidMap.Values()), getTextTitle) } func addListValues(zidMap *set.Set[string], m *meta.Meta, key string) { for val := range m.GetFields(key) { zidMap.Add(val) } } // MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) return } if err = deleteZettel.Run(r.Context(), zid); err != nil { wui.reportError(ctx, w, err) return } wui.redirectFound(w, r, wui.NewURLBuilder('/')) }) } |
Added internal/web/adapter/webui/edit_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "net/http" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" ) // MakeEditGetZettelHandler creates a new HTTP handler to display the // HTML edit view of a zettel. func (wui *WebUI) MakeEditGetZettelHandler( getZettel usecase.GetZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) return } zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { wui.reportError(ctx, w, err) return } roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "", roleData, syntaxData) }) } // MakeEditSetZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) return } reEdit, zettel, err := parseZettelForm(r, zid) hasContent := true if err != nil { if err != errMissingContent { const msg = "Unable to read zettel form" wui.log.Info().Err(err).Msg(msg) wui.reportError(ctx, w, adapter.NewErrBadRequest(msg)) return } hasContent = false } if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil { wui.reportError(ctx, w, err) return } if reEdit { wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(zid)) } else { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid)) } }) } |
Added internal/web/adapter/webui/favicon.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "io" "net/http" "os" "path/filepath" "zettelstore.de/z/internal/web/adapter" ) // MakeFaviconHandler creates a HTTP handler to retrieve the favicon. func (wui *WebUI) MakeFaviconHandler(baseDir string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { filename := filepath.Join(baseDir, "favicon.ico") f, err := os.Open(filename) if err != nil { wui.log.Debug().Err(err).Msg("Favicon not found") http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } defer f.Close() data, err := io.ReadAll(f) if err != nil { wui.log.Error().Err(err).Msg("Unable to read favicon data") http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } if err = adapter.WriteData(w, data, ""); err != nil { wui.log.Error().Err(err).Msg("Write favicon") } }) } |
Added internal/web/adapter/webui/forms.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "bytes" "errors" "io" "net/http" "regexp" "strings" "unicode" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/web/content" "zettelstore.de/z/internal/zettel" ) var ( bsCRLF = []byte{'\r', '\n'} bsLF = []byte{'\n'} ) var errMissingContent = errors.New("missing zettel content") func parseZettelForm(r *http.Request, zid id.Zid) (bool, zettel.Zettel, error) { maxRequestSize := kernel.Main.GetConfig(kernel.WebService, kernel.WebMaxRequestSize).(int64) err := r.ParseMultipartForm(maxRequestSize) if err != nil { return false, zettel.Zettel{}, err } _, doSave := r.Form["save"] var m *meta.Meta if postMeta, ok := trimmedFormValue(r, "meta"); ok { m = meta.NewFromInput(zid, input.NewInput(removeEmptyLines([]byte(postMeta)))) m.Sanitize() } else { m = meta.New(zid) } if postTitle, ok := trimmedFormValue(r, "title"); ok { m.Set(meta.KeyTitle, meta.Value(meta.RemoveNonGraphic(postTitle))) } if postTags, ok := trimmedFormValue(r, "tags"); ok { if tags := meta.Value(meta.RemoveNonGraphic(postTags)).AsSlice(); len(tags) > 0 { for i, tag := range tags { tags[i] = string(meta.Value(tag).NormalizeTag()) } m.SetList(meta.KeyTags, tags) } } if postRole, ok := trimmedFormValue(r, "role"); ok { m.SetWord(meta.KeyRole, meta.RemoveNonGraphic(postRole)) } if postSyntax, ok := trimmedFormValue(r, "syntax"); ok { m.SetWord(meta.KeySyntax, meta.RemoveNonGraphic(postSyntax)) } if data := textContent(r); data != nil { return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(data)}, nil } if data, m2 := uploadedContent(r, m); data != nil { return doSave, zettel.Zettel{Meta: m2, Content: zettel.NewContent(data)}, nil } if allowEmptyContent(m) { return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}, nil } return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}, errMissingContent } func trimmedFormValue(r *http.Request, key string) (string, bool) { if values, ok := r.PostForm[key]; ok && len(values) > 0 { value := strings.TrimSpace(values[0]) if len(value) > 0 { return value, true } } return "", false } func textContent(r *http.Request) []byte { if values, found := r.PostForm["content"]; found && len(values) > 0 { result := bytes.ReplaceAll([]byte(values[0]), bsCRLF, bsLF) if bytes.IndexFunc(result, func(ch rune) bool { return !unicode.IsSpace(ch) }) >= 0 { return result } } return nil } func uploadedContent(r *http.Request, m *meta.Meta) ([]byte, *meta.Meta) { file, fh, err := r.FormFile("file") if file != nil { defer file.Close() if err == nil { data, err2 := io.ReadAll(file) if err2 != nil { return nil, m } if cts, found := fh.Header["Content-Type"]; found && len(cts) > 0 { ct := cts[0] if fileSyntax := content.SyntaxFromMIME(ct, data); fileSyntax != "" { m = m.Clone() m.Set(meta.KeySyntax, meta.Value(fileSyntax)) } } return data, m } } return nil, m } func allowEmptyContent(m *meta.Meta) bool { if syntax, found := m.Get(meta.KeySyntax); found { if syntax == meta.ValueSyntaxNone { return true } if pinfo := parser.Get(string(syntax)); pinfo != nil { return pinfo.IsTextFormat } } return true } var reEmptyLines = regexp.MustCompile(`(\n|\r)+\s*(\n|\r)+`) func removeEmptyLines(s []byte) []byte { b := bytes.TrimSpace(s) return reEmptyLines.ReplaceAllLiteral(b, []byte{'\n'}) } |
Added internal/web/adapter/webui/forms_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import "testing" func TestRemoveEmptyLines(t *testing.T) { t.Parallel() testcases := []struct { in string exp string }{ {"", ""}, {"a", "a"}, {"\na", "a"}, {"a\n", "a"}, {"a\nb", "a\nb"}, {"a\n\nb", "a\nb"}, {"a\n \nb", "a\nb"}, } for i, tc := range testcases { got := string(removeEmptyLines([]byte(tc.in))) if got != tc.exp { t.Errorf("%d/%q: expected=%q, got=%q", i, tc.in, tc.exp, got) } } } |
Added internal/web/adapter/webui/get_info.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "context" "net/http" "slices" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/collect" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/evaluator" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/server" "zettelstore.de/z/strfun" ) // MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetInfoHandler( ucParseZettel usecase.ParseZettel, ucEvaluate *usecase.Evaluate, ucGetZettel usecase.GetZettel, ucGetAllZettel usecase.GetAllZettel, ucQuery *usecase.Query, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) return } zn, err := ucParseZettel.Run(ctx, zid, q.Get(meta.KeySyntax)) if err != nil { wui.reportError(ctx, w, err) return } getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel) var lbMetadata sx.ListBuilder for key, val := range zn.Meta.Computed() { sxval := wui.writeHTMLMetaValue(key, val, getTextTitle) lbMetadata.Add(sx.Cons(sx.MakeString(key), sxval)) } summary := collect.References(zn) locLinks, queryLinks, extLinks := wui.splitLocSeaExtLinks(append(summary.Links, summary.Embeds...)) title := ast.NormalizedSpacedText(zn.InhMeta.GetTitle()) phrase := q.Get(api.QueryKeyPhrase) if phrase == "" { phrase = title } unlinkedMeta, err := ucQuery.Run(ctx, createUnlinkedQuery(zid, phrase)) if err != nil { wui.reportError(ctx, w, err) return } enc := wui.getSimpleHTMLEncoder(wui.getConfig(ctx, zn.InhMeta, meta.KeyLang)) entries, _ := evaluator.QueryAction(ctx, nil, unlinkedMeta) bns := ucEvaluate.RunBlockNode(ctx, entries) unlinkedContent, _, err := enc.BlocksSxn(&bns) if err != nil { wui.reportError(ctx, w, err) return } encTexts := encodingTexts() shadowLinks := getShadowLinks(ctx, zid, zn.InhMeta.GetDefault(meta.KeyBoxNumber, ""), ucGetAllZettel) user := server.GetUser(ctx) env, rb := wui.createRenderEnv(ctx, "info", wui.getUserLang(ctx), title, user) rb.bindString("metadata", lbMetadata.List()) rb.bindString("local-links", locLinks) rb.bindString("query-links", queryLinks) rb.bindString("ext-links", extLinks) rb.bindString("unlinked-content", unlinkedContent) rb.bindString("phrase", sx.MakeString(phrase)) rb.bindString("query-key-phrase", sx.MakeString(api.QueryKeyPhrase)) rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts)) rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts)) rb.bindString("shadow-links", shadowLinks) wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content) if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.ZidInfoTemplate, env) } else { err = rb.err } if err != nil { wui.reportError(ctx, w, err) } }) } func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sx.Pair) { var lbLoc, lbQueries, lbExt sx.ListBuilder for _, ref := range links { switch ref.State { case ast.RefStateHosted, ast.RefStateBased: // Local lbLoc.Add(sx.MakeString(ref.String())) case ast.RefStateQuery: lbQueries.Add( sx.Cons( sx.MakeString(ref.Value), sx.MakeString(wui.NewURLBuilder('h').AppendQuery(ref.Value).String()))) case ast.RefStateExternal: lbExt.Add(sx.MakeString(ref.String())) } } return lbLoc.List(), lbQueries.List(), lbExt.List() } func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query { var sb strings.Builder sb.Write(zid.Bytes()) sb.WriteByte(' ') sb.WriteString(api.UnlinkedDirective) for _, word := range strfun.MakeWords(phrase) { sb.WriteByte(' ') sb.WriteString(api.PhraseDirective) sb.WriteByte(' ') sb.WriteString(word) } sb.WriteByte(' ') sb.WriteString(api.OrderDirective) sb.WriteByte(' ') sb.WriteString(meta.KeyID) return query.Parse(sb.String()) } func encodingTexts() []string { encodings := encoder.GetEncodings() encTexts := make([]string, 0, len(encodings)) for _, f := range encodings { encTexts = append(encTexts, f.String()) } slices.Sort(encTexts) return encTexts } var apiParts = []string{api.PartZettel, api.PartMeta, api.PartContent} func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool, encTexts []string) *sx.Pair { matrix := sx.Nil() u := wui.NewURLBuilder('z').SetZid(zid) for ip := len(apiParts) - 1; ip >= 0; ip-- { part := apiParts[ip] row := sx.Nil() for je := len(encTexts) - 1; je >= 0; je-- { enc := encTexts[je] if parseOnly { u.AppendKVQuery(api.QueryKeyParseOnly, "") } u.AppendKVQuery(api.QueryKeyPart, part) u.AppendKVQuery(api.QueryKeyEncoding, enc) row = row.Cons(sx.Cons(sx.MakeString(enc), sx.MakeString(u.String()))) u.ClearQuery() } matrix = matrix.Cons(sx.Cons(sx.MakeString(part), row)) } return matrix } func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sx.Pair { matrix := wui.infoAPIMatrix(zid, true, encTexts) u := wui.NewURLBuilder('z').SetZid(zid) for i, row := 0, matrix; i < len(apiParts) && row != nil; row = row.Tail() { line, isLine := sx.GetPair(row.Car()) if !isLine || line == nil { continue } last := line.LastPair() part := apiParts[i] u.AppendKVQuery(api.QueryKeyPart, part) last = last.AppendBang(sx.Cons(sx.MakeString("plain"), sx.MakeString(u.String()))) u.ClearQuery() if i < 2 { u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) u.AppendKVQuery(api.QueryKeyPart, part) last.AppendBang(sx.Cons(sx.MakeString("data"), sx.MakeString(u.String()))) u.ClearQuery() } i++ } return matrix } func getShadowLinks(ctx context.Context, zid id.Zid, boxNumber meta.Value, getAllZettel usecase.GetAllZettel) *sx.Pair { var lb sx.ListBuilder if zl, err := getAllZettel.Run(ctx, zid); err == nil { for _, ztl := range zl { if boxNo, ok := ztl.Meta.Get(meta.KeyBoxNumber); ok && boxNo != boxNumber { lb.Add(sx.MakeString(string(boxNo))) } } } return lb.List() } |
Added internal/web/adapter/webui/get_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "context" "net/http" "slices" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/server" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetHTMLZettelHandler( evaluate *usecase.Evaluate, getZettel usecase.GetZettel, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) return } q := r.URL.Query() zn, err := evaluate.Run(ctx, zid, q.Get(meta.KeySyntax)) if err != nil { wui.reportError(ctx, w, err) return } zettelLang := wui.getConfig(ctx, zn.InhMeta, meta.KeyLang) enc := wui.getSimpleHTMLEncoder(zettelLang) metaObj := enc.MetaSxn(zn.InhMeta) content, endnotes, err := enc.BlocksSxn(&zn.BlocksAST) if err != nil { wui.reportError(ctx, w, err) return } user := server.GetUser(ctx) getTextTitle := wui.makeGetTextTitle(ctx, getZettel) title := ast.NormalizedSpacedText(zn.InhMeta.GetTitle()) env, rb := wui.createRenderEnv(ctx, "zettel", zettelLang, title, user) rb.bindSymbol(symMetaHeader, metaObj) rb.bindString("heading", sx.MakeString(title)) if role, found := zn.InhMeta.Get(meta.KeyRole); found && role != "" { rb.bindString( "role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery( meta.KeyRole+api.SearchOperatorHas+string(role)).String())) } if folgeRole, found := zn.InhMeta.Get(meta.KeyFolgeRole); found && folgeRole != "" { rb.bindString( "folge-role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery( meta.KeyRole+api.SearchOperatorHas+string(folgeRole)).String())) } rb.bindString("tag-refs", wui.transformTagSet(meta.KeyTags, zn.InhMeta.GetDefault(meta.KeyTags, "").AsSlice())) rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, meta.KeyPredecessor, getTextTitle)) rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, meta.KeyPrecursor, getTextTitle)) rb.bindString("prequel-refs", wui.identifierSetAsLinks(zn.InhMeta, meta.KeyPrequel, getTextTitle)) rb.bindString("superior-refs", wui.identifierSetAsLinks(zn.InhMeta, meta.KeySuperior, getTextTitle)) rb.bindString("urls", metaURLAssoc(zn.InhMeta)) rb.bindString("content", content) rb.bindString("endnotes", endnotes) wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, meta.KeyFolge, config.KeyShowFolgeLinks, getTextTitle) wui.bindLinks(ctx, &rb, "sequel", zn.InhMeta, meta.KeySequel, config.KeyShowSequelLinks, getTextTitle) wui.bindLinks(ctx, &rb, "subordinate", zn.InhMeta, meta.KeySubordinates, config.KeyShowSubordinateLinks, getTextTitle) wui.bindLinks(ctx, &rb, "back", zn.InhMeta, meta.KeyBack, config.KeyShowBackLinks, getTextTitle) wui.bindLinks(ctx, &rb, "successor", zn.InhMeta, meta.KeySuccessors, config.KeyShowSuccessorLinks, getTextTitle) if role, found := zn.InhMeta.Get(meta.KeyRole); found && role != "" { for _, part := range []string{"meta", "actions", "heading"} { rb.rebindResolved("ROLE-"+string(role)+"-"+part, "ROLE-DEFAULT-"+part) } } wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content) if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.ZidZettelTemplate, env) } else { err = rb.err } if err != nil { wui.reportError(ctx, w, err) } }) } func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair { return wui.transformIdentifierSet(m.GetFields(key), getTextTitle) } func metaURLAssoc(m *meta.Meta) *sx.Pair { var result sx.ListBuilder for key, val := range m.Rest() { if strings.HasSuffix(key, meta.SuffixKeyURL) { if val != "" { result.Add(sx.Cons(sx.MakeString(capitalizeMetaKey(key)), sx.MakeString(string(val)))) } } } return result.List() } func (wui *WebUI) bindLinks(ctx context.Context, rb *renderBinder, varPrefix string, m *meta.Meta, key, configKey string, getTextTitle getTextTitleFunc) { varLinks := varPrefix + "-links" var symOpen *sx.Symbol switch wui.getConfig(ctx, m, configKey) { case "false": rb.bindString(varLinks, sx.Nil()) return case "close": default: symOpen = shtml.SymAttrOpen } lstLinks := wui.zettelLinksSxn(m, key, getTextTitle) rb.bindString(varLinks, lstLinks) if sx.IsNil(lstLinks) { return } rb.bindString(varPrefix+"-open", symOpen) } func (wui *WebUI) zettelLinksSxn(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair { if values := slices.Collect(m.GetFields(key)); len(values) > 0 { return wui.zidLinksSxn(values, getTextTitle) } return nil } func (wui *WebUI) zidLinksSxn(values []string, getTextTitle getTextTitleFunc) *sx.Pair { var lb sx.ListBuilder for _, val := range values { zid, err := id.Parse(val) if err != nil { continue } if title, found := getTextTitle(zid); found > 0 { url := sx.MakeString(wui.NewURLBuilder('h').SetZid(zid).String()) if title == "" { lb.Add(sx.Cons(sx.MakeString(val), url)) } else { lb.Add(sx.Cons(sx.MakeString(title), url)) } } } return lb.List() } |
Added internal/web/adapter/webui/goaction.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "net/http" "zettelstore.de/z/internal/usecase" ) // MakeGetGoActionHandler creates a new HTTP handler to execute certain commands. func (wui *WebUI) MakeGetGoActionHandler(ucRefresh *usecase.Refresh) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Currently, command "refresh" is the only command to be executed. err := ucRefresh.Run(ctx) if err != nil { wui.reportError(ctx, w, err) return } wui.redirectFound(w, r, wui.NewURLBuilder('/')) }) } |
Added internal/web/adapter/webui/home.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "context" "errors" "net/http" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/web/server" "zettelstore.de/z/internal/zettel" ) type getRootPort interface { GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) } // MakeGetRootHandler creates a new HTTP handler to show the root URL. func (wui *WebUI) MakeGetRootHandler(s getRootPort) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if p := r.URL.Path; p != "/" { wui.reportError(ctx, w, adapter.ErrResourceNotFound{Path: p}) return } homeZid, _ := id.Parse(wui.getConfig(ctx, nil, config.KeyHomeZettel)) if homeZid != id.ZidDefaultHome { if _, err := s.GetZettel(ctx, homeZid); err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } homeZid = id.ZidDefaultHome } _, err := s.GetZettel(ctx, homeZid) if err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil { wui.redirectFound(w, r, wui.NewURLBuilder('i')) return } wui.redirectFound(w, r, wui.NewURLBuilder('h')) }) } |
Added internal/web/adapter/webui/htmlgen.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "maps" "net/url" "slices" "strings" "t73f.de/r/sx" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zero/set" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "t73f.de/r/zsc/sz" "t73f.de/r/zsx" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/encoder" ) // Builder allows to build new URLs for the web service. type urlBuilder interface { GetURLPrefix() string NewURLBuilder(key byte) *api.URLBuilder } type htmlGenerator struct { tx encoder.SzTransformer th *shtml.Evaluator lang string symAt *sx.Symbol } func (wui *WebUI) createGenerator(builder urlBuilder, lang string) *htmlGenerator { th := shtml.NewEvaluator(1) findA := func(obj sx.Object) (attr, assoc, rest *sx.Pair) { pair, isPair := sx.GetPair(obj) if !isPair || !shtml.SymA.IsEqual(pair.Car()) { return nil, nil, nil } rest = pair.Tail() if rest == nil { return nil, nil, nil } objA := rest.Car() attr, isPair = sx.GetPair(objA) if !isPair || !sxhtml.SymAttr.IsEqual(attr.Car()) { return nil, nil, nil } return attr, attr.Tail(), rest.Tail() } rebindWrap(th, zsx.SymLink, func(args sx.Vector, env *shtml.Environment, prevFn shtml.EvalFn) sx.Object { refSym, _ := shtml.GetReference(args[1], env) obj := prevFn(args, env) attr, assoc, rest := findA(obj) if attr == nil { return obj } if zsx.SymRefStateExternal.IsEqual(refSym) { a := zsx.GetAttributes(attr) a = a.Set("target", "_blank") a = a.Add("rel", "external").Add("rel", "noreferrer") return rest.Cons(shtml.EvaluateAttributes(a)).Cons(shtml.SymA) } hrefP := assoc.Assoc(shtml.SymAttrHref) if hrefP == nil { return obj } href, ok := sx.GetString(hrefP.Cdr()) if !ok { return obj } switch refSym { case sz.SymRefStateZettel, sz.SymRefStateFound: strZid, fragment, hasFragment := strings.Cut(href.GetValue(), "#") zid, err := id.Parse(strZid) u := builder.NewURLBuilder('h') if err == nil { u = u.SetZid(zid) } if hasFragment { u = u.SetFragment(fragment) } assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String()))) return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) case sz.SymRefStateQuery: ur, err := url.Parse(href.GetValue()) if err != nil { return obj } urlQuery := ur.Query() if !urlQuery.Has(api.QueryKeyQuery) { return obj } u := builder.NewURLBuilder('h') if q := urlQuery.Get(api.QueryKeyQuery); q != "" { u = u.AppendQuery(q) } assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String()))) return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) case sz.SymRefStateBased: u := builder.NewURLBuilder('/') assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String()+href.GetValue()[1:]))) return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) } return obj }) rebind(th, zsx.SymEmbed, func(obj sx.Object) sx.Object { pair, isPair := sx.GetPair(obj) if !isPair || !shtml.SymIMG.IsEqual(pair.Car()) { return obj } attr, isPair := sx.GetPair(pair.Tail().Car()) if !isPair || !sxhtml.SymAttr.IsEqual(attr.Car()) { return obj } srcP := attr.Tail().Assoc(shtml.SymAttrSrc) if srcP == nil { return obj } src, isString := sx.GetString(srcP.Cdr()) if !isString { return obj } zid, err := id.Parse(src.GetValue()) if err != nil { return obj } u := builder.NewURLBuilder('z').SetZid(zid) imgAttr := attr.Tail().Cons(sx.Cons(shtml.SymAttrSrc, sx.MakeString(u.String()))).Cons(sxhtml.SymAttr) return pair.Tail().Tail().Cons(imgAttr).Cons(shtml.SymIMG) }) return &htmlGenerator{ tx: encoder.NewSzTransformer(), th: th, lang: lang, } } func rebind(ev *shtml.Evaluator, sym *sx.Symbol, fn func(sx.Object) sx.Object) { prevFn := ev.ResolveBinding(sym) ev.Rebind(sym, func(args sx.Vector, env *shtml.Environment) sx.Object { obj := prevFn(args, env) if env.GetError() == nil { return fn(obj) } return sx.Nil() }) } func rebindWrap(ev *shtml.Evaluator, sym *sx.Symbol, fn func(sx.Vector, *shtml.Environment, shtml.EvalFn) sx.Object) { prevFn := ev.ResolveBinding(sym) ev.Rebind(sym, func(args sx.Vector, env *shtml.Environment) sx.Object { obj := fn(args, env, prevFn) if env.GetError() == nil { return obj } return sx.Nil() }) } // SetUnique sets a prefix to make several HTML ids unique. func (g *htmlGenerator) SetUnique(s string) *htmlGenerator { g.th.SetUnique(s); return g } var mapMetaKey = map[string]string{ meta.KeyCopyright: "copyright", meta.KeyLicense: "license", } func (g *htmlGenerator) MetaSxn(m *meta.Meta) *sx.Pair { tm := g.tx.GetMeta(m) env := shtml.MakeEnvironment(g.lang) hm, err := g.th.Evaluate(tm, &env) if err != nil { return nil } ignore := set.New(meta.KeyTitle, meta.KeyLang) metaMap := make(map[string]*sx.Pair, 32) if tags, ok := m.Get(meta.KeyTags); ok { metaMap[meta.KeyTags] = g.transformMetaTags(tags) ignore.Add(meta.KeyTags) } for elem := range hm.Values() { mlst, isPair := sx.GetPair(elem) if !isPair { continue } att, isPair := sx.GetPair(mlst.Tail().Car()) if !isPair { continue } if !att.Car().IsEqual(g.symAt) { continue } a := make(zsx.Attributes, 32) for aelem := range att.Tail().Values() { if p, ok := sx.GetPair(aelem); ok { key := p.Car() val := p.Cdr() if tail, isTail := sx.GetPair(val); isTail { val = tail.Car() } a = a.Set(zsx.GoValue(key), zsx.GoValue(val)) } } name, found := a.Get("name") if !found || ignore.Contains(name) { continue } newName, found := mapMetaKey[name] if !found { continue } a = a.Set("name", newName) metaMap[newName] = g.th.EvaluateMeta(a) } var lb sx.ListBuilder for _, key := range slices.Sorted(maps.Keys(metaMap)) { lb.Add(metaMap[key]) } return lb.List() } func (g *htmlGenerator) transformMetaTags(tags meta.Value) *sx.Pair { var sb strings.Builder first := true for val := range tags.Elems() { if !first { sb.WriteString(", ") } first = false sb.WriteString(string(val.CleanTag())) } metaTags := sb.String() if len(metaTags) == 0 { return nil } return g.th.EvaluateMeta(zsx.Attributes{"name": "keywords", "content": metaTags}) } func (g *htmlGenerator) BlocksSxn(bs *ast.BlockSlice) (content, endnotes *sx.Pair, _ error) { if bs == nil || len(*bs) == 0 { return nil, nil, nil } sx := g.tx.GetSz(bs) env := shtml.MakeEnvironment(g.lang) sh, err := g.th.Evaluate(sx, &env) if err != nil { return nil, nil, err } return sh, shtml.Endnotes(&env), nil } // InlinesSxHTML returns an inline slice, encoded as a SxHTML object. func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair { if is == nil || len(*is) == 0 { return nil } return g.nodeSxHTML(is) } func (g *htmlGenerator) nodeSxHTML(node ast.Node) *sx.Pair { sz := g.tx.GetSz(node) env := shtml.MakeEnvironment(g.lang) sh, err := g.th.Evaluate(sz, &env) if err != nil { return nil } return sh } |
Added internal/web/adapter/webui/htmlmeta.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "context" "errors" "iter" "t73f.de/r/sx" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/usecase" ) func (wui *WebUI) writeHTMLMetaValue( key string, value meta.Value, getTextTitle getTextTitleFunc, ) sx.Object { switch kt := meta.Type(key); kt { case meta.TypeCredential: return sx.MakeString(string(value)) case meta.TypeEmpty: return sx.MakeString(string(value)) case meta.TypeID: return wui.transformIdentifier(value, getTextTitle) case meta.TypeIDSet: return wui.transformIdentifierSet(value.Fields(), getTextTitle) case meta.TypeNumber: return wui.transformKeyValueText(key, value, string(value)) case meta.TypeString: return sx.MakeString(string(value)) case meta.TypeTagSet: return wui.transformTagSet(key, value.AsSlice()) case meta.TypeTimestamp: if ts, ok := value.AsTime(); ok { return sx.MakeList( sx.MakeSymbol("time"), sx.MakeList( sxhtml.SymAttr, sx.Cons(sx.MakeSymbol("datetime"), sx.MakeString(ts.Format("2006-01-02T15:04:05"))), ), sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(ts.Format("2006-01-02 15:04:05"))), ) } return sx.Nil() case meta.TypeURL: return wui.url2html(sx.MakeString(string(value))) case meta.TypeWord: return wui.transformKeyValueText(key, value, string(value)) default: return sx.MakeList(shtml.SymSTRONG, sx.MakeString("Unhandled type: "), sx.MakeString(kt.Name)) } } func (wui *WebUI) transformIdentifier(val meta.Value, getTextTitle getTextTitleFunc) sx.Object { text := sx.MakeString(string(val)) zid, err := id.Parse(string(val)) if err != nil { return text } title, found := getTextTitle(zid) switch { case found > 0: ub := wui.NewURLBuilder('h').SetZid(zid) attrs := sx.Nil() if title != "" { attrs = attrs.Cons(sx.Cons(shtml.SymAttrTitle, sx.MakeString(title))) } attrs = attrs.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String()))).Cons(sxhtml.SymAttr) return sx.Nil().Cons(sx.MakeString(zid.String())).Cons(attrs).Cons(shtml.SymA) case found == 0: return sx.MakeList(sx.MakeSymbol("s"), text) default: // case found < 0: return text } } var space = sx.MakeString(" ") func (wui *WebUI) transformIdentifierSet(vals iter.Seq[string], getTextTitle getTextTitleFunc) *sx.Pair { var lb sx.ListBuilder lb.Add(shtml.SymSPAN) hadValue := false for val := range vals { if hadValue { lb.Add(space) } hadValue = true lb.Add(wui.transformIdentifier(meta.Value(val), getTextTitle)) } if hadValue { return lb.List() } return nil } func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair { var lb sx.ListBuilder lb.Add(shtml.SymSPAN) for i, tag := range tags { if i > 0 { lb.Add(space) } lb.Add(wui.transformKeyValueText(key, meta.Value(tag), tag)) } if len(tags) > 1 { lb.AddN(space, wui.transformKeyValuesText(key, tags, "(all)")) } return lb.List() } func (wui *WebUI) transformKeyValueText(key string, value meta.Value, text string) *sx.Pair { ub := wui.NewURLBuilder('h').AppendQuery(key + api.SearchOperatorHas + string(value)) return buildHref(ub, text) } func (wui *WebUI) transformKeyValuesText(key string, values []string, text string) *sx.Pair { ub := wui.NewURLBuilder('h') for _, val := range values { ub = ub.AppendQuery(key + api.SearchOperatorHas + val) } return buildHref(ub, text) } func buildHref(ub *api.URLBuilder, text string) *sx.Pair { return sx.MakeList( shtml.SymA, sx.MakeList( sxhtml.SymAttr, sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String())), ), sx.MakeString(text), ) } type getTextTitleFunc func(id.Zid) (string, int) func (wui *WebUI) makeGetTextTitle(ctx context.Context, getZettel usecase.GetZettel) getTextTitleFunc { return func(zid id.Zid) (string, int) { z, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return "", -1 } return "", 0 } return ast.NormalizedSpacedText(z.Meta.GetTitle()), 1 } } |
Added internal/web/adapter/webui/lists.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "context" "net/http" "net/url" "slices" "strconv" "strings" "t73f.de/r/sx" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/evaluator" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/web/server" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML. func (wui *WebUI) MakeListHTMLMetaHandler( queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { urlQuery := r.URL.Query() if wui.handleTagZettel(w, r, tagZettel, urlQuery) || wui.handleRoleZettel(w, r, roleZettel, urlQuery) { return } q := adapter.GetQuery(urlQuery) q = q.SetDeterministic() ctx := r.Context() metaSeq, err := queryMeta.Run(ctx, q) if err != nil { wui.reportError(ctx, w, err) return } actions, err := adapter.TryReIndex(ctx, q.Actions(), metaSeq, reIndex) if err != nil { wui.reportError(ctx, w, err) return } if len(metaSeq) > 0 { if slices.Contains(actions, api.RedirectAction) { ub := wui.NewURLBuilder('h').SetZid(metaSeq[0].Zid) wui.redirectFound(w, r, ub) return } } userLang := wui.getUserLang(ctx) var content, endnotes *sx.Pair numEntries := 0 if bn, cnt := evaluator.QueryAction(ctx, q, metaSeq); bn != nil { enc := wui.getSimpleHTMLEncoder(userLang) content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn}) if err != nil { wui.reportError(ctx, w, err) return } numEntries = cnt } siteName := wui.rtConfig.GetSiteName() user := server.GetUser(ctx) env, rb := wui.createRenderEnv(ctx, "list", userLang, siteName, user) if q == nil { rb.bindString("heading", sx.MakeString(siteName)) } else { var sb strings.Builder q.PrintHuman(&sb) rb.bindString("heading", sx.MakeString(sb.String())) } rb.bindString("query-value", sx.MakeString(q.String())) if tzl := q.GetMetaValues(meta.KeyTags, false); len(tzl) > 0 { sxTzl, sxNoTzl := wui.transformTagZettelList(ctx, tagZettel, tzl) if !sx.IsNil(sxTzl) { rb.bindString("tag-zettel", sxTzl) } if !sx.IsNil(sxNoTzl) && wui.canCreate(ctx, user) { rb.bindString("create-tag-zettel", sxNoTzl) } } if rzl := q.GetMetaValues(meta.KeyRole, false); len(rzl) > 0 { sxRzl, sxNoRzl := wui.transformRoleZettelList(ctx, roleZettel, rzl) if !sx.IsNil(sxRzl) { rb.bindString("role-zettel", sxRzl) } if !sx.IsNil(sxNoRzl) && wui.canCreate(ctx, user) { rb.bindString("create-role-zettel", sxNoRzl) } } rb.bindString("content", content) rb.bindString("endnotes", endnotes) rb.bindString("num-entries", sx.Int64(numEntries)) rb.bindString("num-meta", sx.Int64(len(metaSeq))) apiURL := wui.NewURLBuilder('z').AppendQuery(q.String()) seed, found := q.GetSeed() if found { apiURL = apiURL.AppendKVQuery(api.QueryKeySeed, strconv.Itoa(seed)) } else { seed = 0 } if len(metaSeq) > 0 { rb.bindString("plain-url", sx.MakeString(apiURL.String())) rb.bindString("data-url", sx.MakeString(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String())) if wui.canCreate(ctx, user) { rb.bindString("create-url", sx.MakeString(wui.createNewURL)) rb.bindString("seed", sx.Int64(seed)) } } if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.ZidListTemplate, env) } else { err = rb.err } if err != nil { wui.reportError(ctx, w, err) } }) } func (wui *WebUI) transformTagZettelList(ctx context.Context, tagZettel *usecase.TagZettel, tags []meta.Value) (withZettel, withoutZettel *sx.Pair) { slices.Reverse(tags) for _, tag := range tags { tag = tag.NormalizeTag() if _, err := tagZettel.Run(ctx, tag); err == nil { u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyTag, string(tag)) withZettel = wui.prependZettelLink(withZettel, string(tag), u) } else { u := wui.NewURLBuilder('c').SetZid(id.ZidTemplateNewTag).AppendKVQuery( queryKeyAction, valueActionNew).AppendKVQuery(meta.KeyTitle, string(tag)) withoutZettel = wui.prependZettelLink(withoutZettel, string(tag), u) } } return withZettel, withoutZettel } func (wui *WebUI) transformRoleZettelList(ctx context.Context, roleZettel *usecase.RoleZettel, roles []meta.Value) (withZettel, withoutZettel *sx.Pair) { slices.Reverse(roles) for _, role := range roles { if _, err := roleZettel.Run(ctx, role); err == nil { u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyRole, string(role)) withZettel = wui.prependZettelLink(withZettel, string(role), u) } else { u := wui.NewURLBuilder('c').SetZid(id.ZidTemplateNewRole).AppendKVQuery( queryKeyAction, valueActionNew).AppendKVQuery(meta.KeyTitle, string(role)) withoutZettel = wui.prependZettelLink(withoutZettel, string(role), u) } } return withZettel, withoutZettel } func (wui *WebUI) prependZettelLink(sxZtl *sx.Pair, name string, u *api.URLBuilder) *sx.Pair { link := sx.MakeList( shtml.SymA, sx.MakeList( sxhtml.SymAttr, sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())), ), sx.MakeString(name), ) if sxZtl != nil { sxZtl = sxZtl.Cons(sx.MakeString(", ")) } return sxZtl.Cons(link) } func (wui *WebUI) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool { tag := vals.Get(api.QueryKeyTag) if tag == "" { return false } ctx := r.Context() z, err := tagZettel.Run(ctx, meta.Value(tag)) if err != nil { wui.reportError(ctx, w, err) return true } wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(z.Meta.Zid)) return true } func (wui *WebUI) handleRoleZettel(w http.ResponseWriter, r *http.Request, roleZettel *usecase.RoleZettel, vals url.Values) bool { role := vals.Get(api.QueryKeyRole) if role == "" { return false } ctx := r.Context() z, err := roleZettel.Run(ctx, meta.Value(role)) if err != nil { wui.reportError(ctx, w, err) return true } wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(z.Meta.Zid)) return true } |
Added internal/web/adapter/webui/login.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "context" "net/http" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" ) // MakeGetLoginOutHandler creates a new HTTP handler to display the HTML login view, // or to execute a logout. func (wui *WebUI) MakeGetLoginOutHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() if query.Has("logout") { wui.clearToken(r.Context(), w) wui.redirectFound(w, r, wui.NewURLBuilder('/')) return } wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false) }) } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { env, rb := wui.createRenderEnv(ctx, "login", wui.getUserLang(ctx), "Login", nil) rb.bindString("retry", sx.MakeBoolean(retry)) if rb.err == nil { rb.err = wui.renderSxnTemplate(ctx, w, id.ZidLoginTemplate, env) } if err := rb.err; err != nil { wui.reportError(ctx, w, err) } } // MakePostLoginHandler creates a new HTTP handler to authenticate the given user. func (wui *WebUI) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !wui.authz.WithAuth() { wui.redirectFound(w, r, wui.NewURLBuilder('/')) return } ctx := r.Context() ident, cred, ok := adapter.GetCredentialsViaForm(r) if !ok { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read login form")) return } token, err := ucAuth.Run(ctx, r, ident, cred, wui.tokenLifetime, auth.KindwebUI) if err != nil { wui.reportError(ctx, w, err) return } if token == nil { wui.renderLoginForm(wui.clearToken(ctx, w), w, true) return } wui.setToken(w, token) wui.redirectFound(w, r, wui.NewURLBuilder('/')) }) } |
Added internal/web/adapter/webui/meta.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | //----------------------------------------------------------------------------- // Copyright (c) 2024-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2024-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "strings" "unicode" "unicode/utf8" ) func capitalizeMetaKey(key string) string { var sb strings.Builder for i, word := range strings.Split(key, "-") { if i > 0 { sb.WriteByte(' ') } if newWord, isSpecial := specialWords[word]; isSpecial { if newWord == "" { sb.WriteString(strings.ToTitle(word)) } else { sb.WriteString(newWord) } continue } r, size := utf8.DecodeRuneInString(word) if r == utf8.RuneError { sb.WriteString(word) continue } sb.WriteRune(unicode.ToTitle(r)) sb.WriteString(word[size:]) } return sb.String() } var specialWords = map[string]string{ "css": "", "html": "", "github": "GitHub", "http": "", "https": "", "pdf": "", "svg": "", "url": "", } |
Added internal/web/adapter/webui/meta_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | //----------------------------------------------------------------------------- // Copyright (c) 2024-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2024-present Detlef Stern //----------------------------------------------------------------------------- package webui import "testing" func TestCapitalizeMetaKey(t *testing.T) { var testcases = []struct { key string exp string }{ {"", ""}, {"alt-url", "Alt URL"}, {"author", "Author"}, {"back", "Back"}, {"box-number", "Box Number"}, {"cite-key", "Cite Key"}, {"fedi-url", "Fedi URL"}, {"github-url", "GitHub URL"}, {"hshn-bib", "Hshn Bib"}, {"job-url", "Job URL"}, {"new-user-id", "New User Id"}, {"origin-zid", "Origin Zid"}, {"site-url", "Site URL"}, } for _, tc := range testcases { t.Run(tc.key, func(t *testing.T) { got := capitalizeMetaKey(tc.key) if got != tc.exp { t.Errorf("capitalize(%q) == %q, but got %q", tc.key, tc.exp, got) } }) } } |
Added internal/web/adapter/webui/response.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "net/http" "t73f.de/r/zsc/api" ) func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) { us := ub.String() wui.log.Debug().Str("uri", us).Msg("redirect") http.Redirect(w, r, us, http.StatusFound) } |
Added internal/web/adapter/webui/sxn_code.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | //----------------------------------------------------------------------------- // Copyright (c) 2023-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "context" "fmt" "io" "t73f.de/r/sx/sxeval" "t73f.de/r/zero/graph" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" ) func (wui *WebUI) loadAllSxnCodeZettel(ctx context.Context) (graph.Digraph[id.Zid], *sxeval.Binding, error) { // getMeta MUST currently use GetZettel, because GetMeta just uses the // Index, which might not be current. getMeta := func(ctx context.Context, zid id.Zid) (*meta.Meta, error) { z, err := wui.box.GetZettel(ctx, zid) if err != nil { return nil, err } return z.Meta, nil } dg := buildSxnCodeDigraph(ctx, id.ZidSxnStart, getMeta) if dg == nil { return nil, wui.rootBinding, nil } dg = dg.AddVertex(id.ZidSxnBase).AddEdge(id.ZidSxnStart, id.ZidSxnBase) dg = dg.AddVertex(id.ZidSxnPrelude).AddEdge(id.ZidSxnBase, id.ZidSxnPrelude) dg = dg.TransitiveClosure(id.ZidSxnStart) if zid, isDAG := dg.IsDAG(); !isDAG { return nil, nil, fmt.Errorf("zettel %v is part of a dependency cycle", zid) } bind := wui.rootBinding.MakeChildBinding("zettel", 128) for _, zid := range dg.SortReverse() { if err := wui.loadSxnCodeZettel(ctx, zid, bind); err != nil { return nil, nil, err } } return dg, bind, nil } type getMetaFunc func(context.Context, id.Zid) (*meta.Meta, error) func buildSxnCodeDigraph(ctx context.Context, startZid id.Zid, getMeta getMetaFunc) graph.Digraph[id.Zid] { m, err := getMeta(ctx, startZid) if err != nil { return nil } var marked *idset.Set stack := []*meta.Meta{m} dg := graph.Digraph[id.Zid](nil).AddVertex(startZid) for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 { curr := stack[pos] stack = stack[:pos] if marked.Contains(curr.Zid) { continue } marked = marked.Add(curr.Zid) for pre := range curr.GetFields(meta.KeyPrecursor) { if preZid, errParse := id.Parse(pre); errParse == nil { m, err = getMeta(ctx, preZid) if err != nil { continue } stack = append(stack, m) dg.AddVertex(preZid) dg.AddEdge(curr.Zid, preZid) } } } return dg } func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid, bind *sxeval.Binding) error { rdr, err := wui.makeZettelReader(ctx, zid) if err != nil { return err } env := sxeval.MakeExecutionEnvironment(bind) for { form, err2 := rdr.Read() if err2 != nil { if err2 == io.EOF { return nil } return err2 } wui.log.Debug().Zid(zid).Str("form", form.String()).Msg("Loaded sxn code") if _, err2 = env.Eval(form); err2 != nil { return err2 } } } |
Added internal/web/adapter/webui/template.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "bytes" "context" "fmt" "net/http" "net/url" "t73f.de/r/sx" "t73f.de/r/sx/sxbuiltins" "t73f.de/r/sx/sxeval" "t73f.de/r/sx/sxreader" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/collect" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/web/server" "zettelstore.de/z/internal/zettel" ) func (wui *WebUI) createRenderBinding() *sxeval.Binding { root := sxeval.MakeRootBinding(len(specials) + len(builtins) + 3) for _, syntax := range specials { root.BindSpecial(syntax) } for _, b := range builtins { root.BindBuiltin(b) } _ = root.Bind(sx.MakeSymbol("NIL"), sx.Nil()) _ = root.Bind(sx.MakeSymbol("T"), sx.MakeSymbol("T")) root.BindBuiltin(&sxeval.Builtin{ Name: "url-to-html", MinArity: 1, MaxArity: 1, TestPure: sxeval.AssertPure, Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) { text, err := sxbuiltins.GetString(arg, 0) if err != nil { return nil, err } return wui.url2html(text), nil }, }) root.BindBuiltin(&sxeval.Builtin{ Name: "zid-content-path", MinArity: 1, MaxArity: 1, TestPure: sxeval.AssertPure, Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) { s, err := sxbuiltins.GetString(arg, 0) if err != nil { return nil, err } zid, err := id.Parse(s.GetValue()) if err != nil { return nil, fmt.Errorf("parsing zettel identifier %q: %w", s.GetValue(), err) } ub := wui.NewURLBuilder('z').SetZid(zid) return sx.MakeString(ub.String()), nil }, }) root.BindBuiltin(&sxeval.Builtin{ Name: "query->url", MinArity: 1, MaxArity: 1, TestPure: sxeval.AssertPure, Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) { qs, err := sxbuiltins.GetString(arg, 0) if err != nil { return nil, err } u := wui.NewURLBuilder('h').AppendQuery(qs.GetValue()) return sx.MakeString(u.String()), nil }, }) root.Freeze() return root } var ( specials = []*sxeval.Special{ &sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote &sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing &sxbuiltins.DefVarS, // defvar &sxbuiltins.DefunS, &sxbuiltins.LambdaS, // defun, lambda &sxbuiltins.SetXS, // set! &sxbuiltins.IfS, // if &sxbuiltins.BeginS, // begin &sxbuiltins.DefMacroS, // defmacro &sxbuiltins.LetS, // let } builtins = []*sxeval.Builtin{ &sxbuiltins.Equal, // = &sxbuiltins.NumGreater, // > &sxbuiltins.NullP, // null? &sxbuiltins.PairP, // pair? &sxbuiltins.Car, &sxbuiltins.Cdr, // car, cdr &sxbuiltins.Caar, &sxbuiltins.Cadr, &sxbuiltins.Cdar, &sxbuiltins.Cddr, &sxbuiltins.Caaar, &sxbuiltins.Caadr, &sxbuiltins.Cadar, &sxbuiltins.Caddr, &sxbuiltins.Cdaar, &sxbuiltins.Cdadr, &sxbuiltins.Cddar, &sxbuiltins.Cdddr, &sxbuiltins.List, // list &sxbuiltins.Append, // append &sxbuiltins.Assoc, // assoc &sxbuiltins.Map, // map &sxbuiltins.Apply, // apply &sxbuiltins.Concat, // concat &sxbuiltins.BoundP, // bound? &sxbuiltins.DefinedP, // defined? &sxbuiltins.CurrentBinding, // current-binding &sxbuiltins.BindingLookup, // binding-lookup } ) func (wui *WebUI) url2html(text sx.String) sx.Object { if u, errURL := url.Parse(text.GetValue()); errURL == nil { if us := u.String(); us != "" { return sx.MakeList( shtml.SymA, sx.MakeList( sxhtml.SymAttr, sx.Cons(shtml.SymAttrHref, sx.MakeString(us)), sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank")), sx.Cons(shtml.SymAttrRel, sx.MakeString("external noreferrer")), ), text) } } return text } func (wui *WebUI) getParentEnv(ctx context.Context) (*sxeval.Binding, error) { wui.mxZettelBinding.Lock() defer wui.mxZettelBinding.Unlock() if parentEnv := wui.zettelBinding; parentEnv != nil { return parentEnv, nil } dag, zettelEnv, err := wui.loadAllSxnCodeZettel(ctx) if err != nil { wui.log.Error().Err(err).Msg("loading zettel sxn") return nil, err } wui.dag = dag wui.zettelBinding = zettelEnv return zettelEnv, nil } // createRenderEnv creates a new environment and populates it with all relevant data for the base template. func (wui *WebUI) createRenderEnv(ctx context.Context, name, lang, title string, user *meta.Meta) (*sxeval.Binding, renderBinder) { userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user) parentEnv, err := wui.getParentEnv(ctx) bind := parentEnv.MakeChildBinding(name, 128) rb := makeRenderBinder(bind, err) rb.bindString("lang", sx.MakeString(lang)) rb.bindString("css-base-url", sx.MakeString(wui.cssBaseURL)) rb.bindString("css-user-url", sx.MakeString(wui.cssUserURL)) rb.bindString("title", sx.MakeString(title)) rb.bindString("home-url", sx.MakeString(wui.homeURL)) rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth)) rb.bindString("user-is-valid", sx.MakeBoolean(userIsValid)) rb.bindString("user-zettel-url", sx.MakeString(userZettelURL)) rb.bindString("user-ident", sx.MakeString(userIdent)) rb.bindString("login-url", sx.MakeString(wui.loginURL)) rb.bindString("logout-url", sx.MakeString(wui.logoutURL)) rb.bindString("list-urls", wui.buildListsMenuSxn(ctx, lang)) if wui.canRefresh(user) { rb.bindString("refresh-url", sx.MakeString(wui.refreshURL)) } rb.bindString("new-zettel-links", wui.fetchNewTemplatesSxn(ctx, user)) rb.bindString("search-url", sx.MakeString(wui.searchURL)) rb.bindString("query-key-query", sx.MakeString(api.QueryKeyQuery)) rb.bindString("query-key-seed", sx.MakeString(api.QueryKeySeed)) rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer rb.bindString("debug-mode", sx.MakeBoolean(wui.debug)) rb.bindSymbol(symMetaHeader, sx.Nil()) rb.bindSymbol(symDetail, sx.Nil()) return bind, rb } func (wui *WebUI) getUserRenderData(user *meta.Meta) (bool, string, string) { if user == nil { return false, "", "" } return true, wui.NewURLBuilder('h').SetZid(user.Zid).String(), string(user.GetDefault(meta.KeyUserID, "")) } type renderBinder struct { err error binding *sxeval.Binding } func makeRenderBinder(bind *sxeval.Binding, err error) renderBinder { return renderBinder{binding: bind, err: err} } func (rb *renderBinder) bindString(key string, obj sx.Object) { if rb.err == nil { rb.err = rb.binding.Bind(sx.MakeSymbol(key), obj) } } func (rb *renderBinder) bindSymbol(sym *sx.Symbol, obj sx.Object) { if rb.err == nil { rb.err = rb.binding.Bind(sym, obj) } } func (rb *renderBinder) bindKeyValue(key string, value meta.Value) { rb.bindString("meta-"+key, sx.MakeString(string(value))) if kt := meta.Type(key); kt.IsSet { rb.bindString("set-meta-"+key, makeStringList(value.AsSlice())) } } func (rb *renderBinder) rebindResolved(key, defKey string) { if rb.err == nil { if obj, found := rb.binding.Resolve(sx.MakeSymbol(key)); found { rb.bindString(defKey, obj) } } } func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) { zid := m.Zid strZid := zid.String() newURLBuilder := wui.NewURLBuilder rb.bindString("zid", sx.MakeString(strZid)) rb.bindString("web-url", sx.MakeString(newURLBuilder('h').SetZid(zid).String())) if content != nil && wui.canWrite(ctx, user, m, *content) { rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(zid).String())) } rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(zid).String())) if wui.canCreate(ctx, user) { if content != nil && !content.IsBinary() { rb.bindString("copy-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionCopy).String())) } rb.bindString("version-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionVersion).String())) rb.bindString("sequel-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionSequel).String())) rb.bindString("folge-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionFolge).String())) } if wui.canDelete(ctx, user, m) { rb.bindString("delete-url", sx.MakeString(newURLBuilder('d').SetZid(zid).String())) } if val, found := m.Get(meta.KeyUselessFiles); found { rb.bindString("useless", sx.Cons(sx.MakeString(string(val)), nil)) } queryContext := strZid + " " + api.ContextDirective rb.bindString("context-url", sx.MakeString(newURLBuilder('h').AppendQuery(queryContext).String())) queryContext += " " + api.FullDirective rb.bindString("context-full-url", sx.MakeString(newURLBuilder('h').AppendQuery(queryContext).String())) if wui.canRefresh(user) { rb.bindString("reindex-url", sx.MakeString(newURLBuilder('h').AppendQuery( strZid+" "+api.IdentDirective+api.ActionSeparator+api.ReIndexAction).String())) } // Ensure to have title, role, tags, and syntax included as "meta-*" rb.bindKeyValue(meta.KeyTitle, m.GetDefault(meta.KeyTitle, "")) rb.bindKeyValue(meta.KeyRole, m.GetDefault(meta.KeyRole, "")) rb.bindKeyValue(meta.KeyTags, m.GetDefault(meta.KeyTags, "")) rb.bindKeyValue(meta.KeySyntax, m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)) var metaPairs sx.ListBuilder for key, val := range m.Computed() { metaPairs.Add(sx.Cons(sx.MakeString(key), sx.MakeString(string(val)))) rb.bindKeyValue(key, val) } rb.bindString("metapairs", metaPairs.List()) } func (wui *WebUI) buildListsMenuSxn(ctx context.Context, lang string) *sx.Pair { var zn *ast.ZettelNode if menuZid, err := id.Parse(wui.getConfig(ctx, nil, config.KeyListsMenuZettel)); err == nil { if zn, err = wui.evalZettel.Run(ctx, menuZid, ""); err != nil { zn = nil } } if zn == nil { ctx = box.NoEnrichContext(ctx) ztl, err := wui.box.GetZettel(ctx, id.ZidTOCListsMenu) if err != nil { return nil } zn = wui.evalZettel.RunZettel(ctx, ztl, "") } htmlgen := wui.getSimpleHTMLEncoder(lang) var lb sx.ListBuilder for _, ln := range collect.Order(zn) { lb.Add(htmlgen.nodeSxHTML(ln)) } return lb.List() } func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) *sx.Pair { if !wui.canCreate(ctx, user) { return nil } ctx = box.NoEnrichContext(ctx) menu, err := wui.box.GetZettel(ctx, id.ZidTOCNewTemplate) if err != nil { return nil } var lb sx.ListBuilder for _, ln := range collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig)) { ref := ln.Ref if !ref.IsZettel() { continue } zid, err2 := id.Parse(ref.URL.Path) if err2 != nil { continue } z, err2 := wui.box.GetZettel(ctx, zid) if err2 != nil { continue } if !wui.policy.CanRead(user, z.Meta) { continue } text := sx.MakeString(ast.NormalizedSpacedText(z.Meta.GetTitle())) link := sx.MakeString(wui.NewURLBuilder('c').SetZid(zid). AppendKVQuery(queryKeyAction, valueActionNew).String()) lb.Add(sx.Cons(text, link)) } return lb.List() } func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair { if footerZid, err := id.Parse(wui.getConfig(ctx, nil, config.KeyFooterZettel)); err == nil { if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil { htmlEnc := wui.getSimpleHTMLEncoder(wui.getConfig(ctx, zn.InhMeta, meta.KeyLang)).SetUnique("footer-") if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.BlocksAST); err3 == nil { if content != nil && endnotes != nil { content.LastPair().SetCdr(sx.Cons(endnotes, nil)) } return content } } } return nil } func (wui *WebUI) getSxnTemplate(ctx context.Context, zid id.Zid, bind *sxeval.Binding) (sxeval.Expr, error) { if t := wui.getSxnCache(zid); t != nil { return t, nil } reader, err := wui.makeZettelReader(ctx, zid) if err != nil { return nil, err } objs, err := reader.ReadAll() if err != nil { wui.log.Error().Err(err).Zid(zid).Msg("reading sxn template") return nil, err } if len(objs) != 1 { return nil, fmt.Errorf("expected 1 expression in template, but got %d", len(objs)) } env := sxeval.MakeExecutionEnvironment(bind) t, err := env.Compile(objs[0]) if err != nil { return nil, err } wui.setSxnCache(zid, t) return t, nil } func (wui *WebUI) makeZettelReader(ctx context.Context, zid id.Zid) (*sxreader.Reader, error) { ztl, err := wui.box.GetZettel(ctx, zid) if err != nil { return nil, err } reader := sxreader.MakeReader(bytes.NewReader(ztl.Content.AsBytes())) return reader, nil } func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, bind *sxeval.Binding) (sx.Object, error) { templateExpr, err := wui.getSxnTemplate(ctx, zid, bind) if err != nil { return nil, err } env := sxeval.MakeExecutionEnvironment(bind) return env.Run(templateExpr) } func (wui *WebUI) renderSxnTemplate(ctx context.Context, w http.ResponseWriter, templateID id.Zid, bind *sxeval.Binding) error { return wui.renderSxnTemplateStatus(ctx, w, http.StatusOK, templateID, bind) } func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, bind *sxeval.Binding) error { detailObj, err := wui.evalSxnTemplate(ctx, templateID, bind) if err != nil { return err } bind.Bind(symDetail, detailObj) pageObj, err := wui.evalSxnTemplate(ctx, id.ZidBaseTemplate, bind) if err != nil { return err } if msg := wui.log.Debug(); msg != nil { // pageObj.String() can be expensive to calculate. msg.Str("page", pageObj.String()).Msg("render") } gen := sxhtml.NewGenerator().SetNewline() var sb bytes.Buffer _, err = gen.WriteHTML(&sb, pageObj) if err != nil { return err } wui.prepareAndWriteHeader(w, code) if _, err = w.Write(sb.Bytes()); err != nil { wui.log.Error().Err(err).Msg("Unable to write HTML via template") } return nil // No error reporting, since we do not know what happended during write to client. } func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { ctx = context.WithoutCancel(ctx) // Ignore any cancel / timeouts to write an error message. code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { wui.log.Error().Msg(err.Error()) } else { wui.log.Debug().Err(err).Msg("reportError") } user := server.GetUser(ctx) env, rb := wui.createRenderEnv(ctx, "error", meta.ValueLangEN, "Error", user) rb.bindString("heading", sx.MakeString(http.StatusText(code))) rb.bindString("message", sx.MakeString(text)) if rb.err == nil { rb.err = wui.renderSxnTemplateStatus(ctx, w, code, id.ZidErrorTemplate, env) } errSx := rb.err if errSx == nil { return } wui.log.Error().Err(errSx).Msg("while rendering error message") // if errBind != nil, the HTTP header was not written wui.prepareAndWriteHeader(w, http.StatusInternalServerError) fmt.Fprintf( w, `<!DOCTYPE html> <html> <head><title>Internal server error</title></head> <body> <h1>Internal server error</h1> <p>When generating error code %d with message:</p><pre>%v</pre><p>an error occured:</p><pre>%v</pre> </body> </html>`, code, text, errSx) } func makeStringList(sl []string) *sx.Pair { var lb sx.ListBuilder for _, s := range sl { lb.Add(sx.MakeString(s)) } return lb.List() } |
Added internal/web/adapter/webui/webui.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "net/http" "sync" "time" "t73f.de/r/sx" "t73f.de/r/sx/sxeval" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zero/graph" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logger" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/web/server" "zettelstore.de/z/internal/zettel" ) // WebUI holds all data for delivering the web ui. type WebUI struct { log *logger.Logger debug bool ab server.AuthBuilder authz auth.AuthzManager rtConfig config.Config token auth.TokenManager box webuiBox policy auth.Policy evalZettel *usecase.Evaluate mxCache sync.RWMutex templateCache map[id.Zid]sxeval.Expr tokenLifetime time.Duration cssBaseURL string cssUserURL string homeURL string refreshURL string withAuth bool loginURL string logoutURL string searchURL string createNewURL string rootBinding *sxeval.Binding mxZettelBinding sync.Mutex zettelBinding *sxeval.Binding dag graph.Digraph[id.Zid] genHTML *sxhtml.Generator } // webuiBox contains all box methods that are needed for WebUI operation. // // Note: these function must not do auth checking. type webuiBox interface { CanCreateZettel(context.Context) bool GetZettel(context.Context, id.Zid) (zettel.Zettel, error) GetMeta(context.Context, id.Zid) (*meta.Meta, error) CanUpdateZettel(context.Context, zettel.Zettel) bool CanDeleteZettel(context.Context, id.Zid) bool } // New creates a new WebUI struct. func New(log *logger.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager, mgr box.Manager, pol auth.Policy, evalZettel *usecase.Evaluate) *WebUI { loginoutBase := ab.NewURLBuilder('i') wui := &WebUI{ log: log, debug: kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool), ab: ab, rtConfig: rtConfig, authz: authz, token: token, box: mgr, policy: pol, evalZettel: evalZettel, templateCache: make(map[id.Zid]sxeval.Expr, 32), tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration), cssBaseURL: ab.NewURLBuilder('z').SetZid(id.ZidBaseCSS).String(), cssUserURL: ab.NewURLBuilder('z').SetZid(id.ZidUserCSS).String(), homeURL: ab.NewURLBuilder('/').String(), refreshURL: ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(), withAuth: authz.WithAuth(), loginURL: loginoutBase.String(), logoutURL: loginoutBase.AppendKVQuery("logout", "").String(), searchURL: ab.NewURLBuilder('h').String(), createNewURL: ab.NewURLBuilder('c').String(), zettelBinding: nil, genHTML: sxhtml.NewGenerator().SetNewline(), } wui.rootBinding = wui.createRenderBinding() wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(wui.observe) return wui } func (wui *WebUI) getConfig(ctx context.Context, m *meta.Meta, key string) string { return wui.rtConfig.Get(ctx, m, key) } func (wui *WebUI) getUserLang(ctx context.Context) string { return wui.getConfig(ctx, nil, meta.KeyLang) } var ( symDetail = sx.MakeSymbol("DETAIL") symMetaHeader = sx.MakeSymbol("META-HEADER") ) func (wui *WebUI) observe(ci box.UpdateInfo) { wui.mxCache.Lock() if ci.Reason == box.OnReload { clear(wui.templateCache) } else { delete(wui.templateCache, ci.Zid) } wui.mxCache.Unlock() wui.mxZettelBinding.Lock() if ci.Reason == box.OnReload || wui.dag.HasVertex(ci.Zid) { wui.zettelBinding = nil wui.dag = nil } wui.mxZettelBinding.Unlock() } func (wui *WebUI) setSxnCache(zid id.Zid, expr sxeval.Expr) { wui.mxCache.Lock() wui.templateCache[zid] = expr wui.mxCache.Unlock() } func (wui *WebUI) getSxnCache(zid id.Zid) sxeval.Expr { wui.mxCache.RLock() expr, found := wui.templateCache[zid] wui.mxCache.RUnlock() if found { return expr } return nil } func (wui *WebUI) canCreate(ctx context.Context, user *meta.Meta) bool { m := meta.New(id.Invalid) return wui.policy.CanCreate(user, m) && wui.box.CanCreateZettel(ctx) } func (wui *WebUI) canWrite( ctx context.Context, user, meta *meta.Meta, content zettel.Content) bool { return wui.policy.CanWrite(user, meta, meta) && wui.box.CanUpdateZettel(ctx, zettel.Zettel{Meta: meta, Content: content}) } func (wui *WebUI) canDelete(ctx context.Context, user, m *meta.Meta) bool { return wui.policy.CanDelete(user, m) && wui.box.CanDeleteZettel(ctx, m.Zid) } func (wui *WebUI) canRefresh(user *meta.Meta) bool { return wui.policy.CanRefresh(user) } func (wui *WebUI) getSimpleHTMLEncoder(lang string) *htmlGenerator { return wui.createGenerator(wui, lang) } // GetURLPrefix returns the configured URL prefix of the web server. func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. func (wui *WebUI) NewURLBuilder(key byte) *api.URLBuilder { return wui.ab.NewURLBuilder(key) } func (wui *WebUI) clearToken(ctx context.Context, w http.ResponseWriter) context.Context { return wui.ab.ClearToken(ctx, w) } func (wui *WebUI) setToken(w http.ResponseWriter, token []byte) { wui.ab.SetToken(w, token, wui.tokenLifetime) } func (wui *WebUI) prepareAndWriteHeader(w http.ResponseWriter, statusCode int) { h := adapter.PrepareHeader(w, "text/html; charset=utf-8") h.Set("Content-Security-Policy", "default-src 'self'; img-src * data:; style-src 'self' 'unsafe-inline'") h.Set("Permissions-Policy", "payment=(), interest-cohort=()") h.Set("Referrer-Policy", "no-referrer") h.Set("X-Content-Type-Options", "nosniff") if !wui.debug { h.Set("X-Frame-Options", "sameorigin") } w.WriteHeader(statusCode) } |
Added internal/web/content/content.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- // Package content manages content handling within the web package. // It translates syntax values into content types, and vice versa. package content import ( "mime" "net/http" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/zettel" ) // Some MIME encoding values. const ( UnknownMIME = "application/octet-stream" mimeGIF = "image/gif" mimeHTML = "text/html; charset=utf-8" mimeJPEG = "image/jpeg" mimeMarkdown = "text/markdown; charset=utf-8" PlainText = "text/plain; charset=utf-8" mimePNG = "image/png" SXPF = PlainText mimeWEBP = "image/webp" ) var encoding2mime = map[api.EncodingEnum]string{ api.EncoderHTML: mimeHTML, api.EncoderMD: mimeMarkdown, api.EncoderSz: SXPF, api.EncoderSHTML: SXPF, api.EncoderText: PlainText, api.EncoderZmk: PlainText, } // MIMEFromEncoding returns the MIME encoding for a given zettel encoding func MIMEFromEncoding(enc api.EncodingEnum) string { if m, found := encoding2mime[enc]; found { return m } return UnknownMIME } var syntax2mime = map[string]string{ meta.ValueSyntaxCSS: "text/css; charset=utf-8", meta.ValueSyntaxDraw: PlainText, meta.ValueSyntaxGif: mimeGIF, meta.ValueSyntaxHTML: mimeHTML, meta.ValueSyntaxJPEG: mimeJPEG, meta.ValueSyntaxJPG: mimeJPEG, meta.ValueSyntaxMarkdown: mimeMarkdown, meta.ValueSyntaxMD: mimeMarkdown, meta.ValueSyntaxNone: "", meta.ValueSyntaxPlain: PlainText, meta.ValueSyntaxPNG: mimePNG, meta.ValueSyntaxSVG: "image/svg+xml", meta.ValueSyntaxSxn: SXPF, meta.ValueSyntaxText: PlainText, meta.ValueSyntaxTxt: PlainText, meta.ValueSyntaxWebp: mimeWEBP, meta.ValueSyntaxZmk: "text/x-zmk; charset=utf-8", // Additional syntaxes that are parsed as plain text. "js": "text/javascript; charset=utf-8", "pdf": "application/pdf", "xml": "text/xml; charset=utf-8", } // MIMEFromSyntax returns a MIME encoding for a given syntax value. func MIMEFromSyntax(syntax string) string { if mt, found := syntax2mime[syntax]; found { return mt } return UnknownMIME } var mime2syntax = map[string]string{ mimeGIF: meta.ValueSyntaxGif, mimeJPEG: meta.ValueSyntaxJPEG, mimePNG: meta.ValueSyntaxPNG, mimeWEBP: meta.ValueSyntaxWebp, "text/html": meta.ValueSyntaxHTML, "text/markdown": meta.ValueSyntaxMarkdown, "text/plain": meta.ValueSyntaxText, // Additional syntaxes "application/pdf": "pdf", "text/javascript": "js", } // SyntaxFromMIME returns the syntax for a zettel based on MIME encoding value // and the actual data. func SyntaxFromMIME(m string, data []byte) string { mt, _, _ := mime.ParseMediaType(m) if syntax, found := mime2syntax[mt]; found { return syntax } if len(data) > 0 { ct := http.DetectContentType(data) mt, _, _ = mime.ParseMediaType(ct) if syntax, found := mime2syntax[mt]; found { return syntax } if ext, err := mime.ExtensionsByType(mt); err != nil && len(ext) > 0 { return ext[0][1:] } if zettel.IsBinary(data) { return "binary" } } return "plain" } |
Added internal/web/content/content_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package content_test import ( "testing" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/web/content" ) func TestSupportedSyntax(t *testing.T) { for _, syntax := range parser.GetSyntaxes() { mt := content.MIMEFromSyntax(syntax) if mt == content.UnknownMIME { t.Errorf("No MIME type registered for syntax %q", syntax) continue } newSyntax := content.SyntaxFromMIME(mt, nil) pinfo := parser.Get(newSyntax) if pinfo == nil { t.Errorf("MIME type for syntax %q is %q, but this has no corresponding syntax", syntax, mt) continue } } } |
Added internal/web/server/http.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package server import ( "context" "net" "net/http" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/logger" ) type webServer struct { log *logger.Logger baseURL string httpServer httpServer router httpRouter persistentCookie bool secureCookie bool } // ConfigData contains the data needed to configure a server. type ConfigData struct { Log *logger.Logger ListenAddr string BaseURL string URLPrefix string MaxRequestSize int64 Auth auth.TokenManager PersistentCookie bool SecureCookie bool Profiling bool } // New creates a new web server. func New(sd ConfigData) Server { srv := webServer{ log: sd.Log, baseURL: sd.BaseURL, persistentCookie: sd.PersistentCookie, secureCookie: sd.SecureCookie, } rd := routerData{ log: sd.Log, urlPrefix: sd.URLPrefix, maxRequestSize: sd.MaxRequestSize, auth: sd.Auth, profiling: sd.Profiling, } srv.router.initializeRouter(rd) srv.httpServer.initializeHTTPServer(sd.ListenAddr, &srv.router) return &srv } func (srv *webServer) Handle(pattern string, handler http.Handler) { srv.router.Handle(pattern, handler) } func (srv *webServer) AddListRoute(key byte, method Method, handler http.Handler) { srv.router.addListRoute(key, method, handler) } func (srv *webServer) AddZettelRoute(key byte, method Method, handler http.Handler) { srv.router.addZettelRoute(key, method, handler) } func (srv *webServer) SetUserRetriever(ur UserRetriever) { srv.router.ur = ur } func (srv *webServer) GetURLPrefix() string { return srv.router.urlPrefix } func (srv *webServer) NewURLBuilder(key byte) *api.URLBuilder { return api.NewURLBuilder(srv.GetURLPrefix(), key) } func (srv *webServer) NewURLBuilderAbs(key byte) *api.URLBuilder { return api.NewURLBuilder(srv.baseURL, key) } const sessionName = "zsession" func (srv *webServer) SetToken(w http.ResponseWriter, token []byte, d time.Duration) { cookie := http.Cookie{ Name: sessionName, Value: string(token), Path: srv.GetURLPrefix(), Secure: srv.secureCookie, HttpOnly: true, SameSite: http.SameSiteLaxMode, } if srv.persistentCookie && d > 0 { cookie.Expires = time.Now().Add(d).Add(30 * time.Second).UTC() } srv.log.Debug().Bytes("token", token).Msg("SetToken") if v := cookie.String(); v != "" { w.Header().Add("Set-Cookie", v) w.Header().Add("Cache-Control", `no-cache="Set-Cookie"`) w.Header().Add("Vary", "Cookie") } } // ClearToken invalidates the session cookie by sending an empty one. func (srv *webServer) ClearToken(ctx context.Context, w http.ResponseWriter) context.Context { if authData := GetAuthData(ctx); authData == nil { // No authentication data stored in session, nothing to do. return ctx } if w != nil { srv.SetToken(w, nil, 0) } return updateContext(ctx, nil, nil) } func updateContext(ctx context.Context, user *meta.Meta, data *auth.TokenData) context.Context { if data == nil { return context.WithValue(ctx, ctxKeySession, &AuthData{User: user}) } return context.WithValue( ctx, ctxKeySession, &AuthData{ User: user, Token: data.Token, Now: data.Now, Issued: data.Issued, Expires: data.Expires, }) } func (srv *webServer) Run() error { return srv.httpServer.start() } func (srv *webServer) Stop() { srv.httpServer.stop() } // Server timeout values const shutdownTimeout = 5 * time.Second // httpServer is a HTTP server. type httpServer struct { http.Server } // initializeHTTPServer creates a new HTTP server object. func (srv *httpServer) initializeHTTPServer(addr string, handler http.Handler) { if addr == "" { addr = ":http" } srv.Server = http.Server{ Addr: addr, Handler: handler, } } // start the web server, but does not wait for its completion. func (srv *httpServer) start() error { ln, err := net.Listen("tcp", srv.Addr) if err != nil { return err } go func() { srv.Serve(ln) }() return nil } // stop the web server. func (srv *httpServer) stop() { ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() srv.Shutdown(ctx) } |
Added internal/web/server/router.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package server import ( "io" "net/http" "net/http/pprof" "regexp" rtprf "runtime/pprof" "strings" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/logger" ) type ( methodHandler [methodLAST]http.Handler routingTable [256]*methodHandler ) var mapMethod = map[string]Method{ http.MethodHead: MethodHead, http.MethodGet: MethodGet, http.MethodPost: MethodPost, http.MethodPut: MethodPut, http.MethodDelete: MethodDelete, } // httpRouter handles all routing for zettelstore. type httpRouter struct { log *logger.Logger urlPrefix string auth auth.TokenManager minKey byte maxKey byte reURL *regexp.Regexp listTable routingTable zettelTable routingTable ur UserRetriever mux *http.ServeMux maxReqSize int64 } type routerData struct { log *logger.Logger urlPrefix string maxRequestSize int64 auth auth.TokenManager profiling bool } // initializeRouter creates a new, empty router with the given root handler. func (rt *httpRouter) initializeRouter(rd routerData) { rt.log = rd.log rt.urlPrefix = rd.urlPrefix rt.auth = rd.auth rt.minKey = 255 rt.maxKey = 0 rt.reURL = regexp.MustCompile("^$") rt.mux = http.NewServeMux() rt.maxReqSize = rd.maxRequestSize if rd.profiling { rt.setRuntimeProfiling() } } func (rt *httpRouter) setRuntimeProfiling() { rt.mux.HandleFunc("GET /rtp/", pprof.Index) for _, profile := range rtprf.Profiles() { name := profile.Name() rt.mux.Handle("GET /rtp/"+name, pprof.Handler(name)) } rt.mux.HandleFunc("GET /rtp/cmdline", pprof.Cmdline) rt.mux.HandleFunc("GET /rtp/profile", pprof.Profile) rt.mux.HandleFunc("GET /rtp/symbol", pprof.Symbol) rt.mux.HandleFunc("GET /rtp/trace", pprof.Trace) } func (rt *httpRouter) addRoute(key byte, method Method, handler http.Handler, table *routingTable) { // Set minKey and maxKey; re-calculate regexp. if key < rt.minKey || rt.maxKey < key { if key < rt.minKey { rt.minKey = key } if rt.maxKey < key { rt.maxKey = key } rt.reURL = regexp.MustCompile( "^/(?:([" + string(rt.minKey) + "-" + string(rt.maxKey) + "])(?:/(?:([0-9]{14})/?)?)?)$") } mh := table[key] if mh == nil { mh = new(methodHandler) table[key] = mh } mh[method] = handler if method == MethodGet { if prevHandler := mh[MethodHead]; prevHandler == nil { mh[MethodHead] = handler } } } // addListRoute adds a route for the given key and HTTP method to work with a list. func (rt *httpRouter) addListRoute(key byte, method Method, handler http.Handler) { rt.addRoute(key, method, handler, &rt.listTable) } // addZettelRoute adds a route for the given key and HTTP method to work with a zettel. func (rt *httpRouter) addZettelRoute(key byte, method Method, handler http.Handler) { rt.addRoute(key, method, handler, &rt.zettelTable) } // Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics. func (rt *httpRouter) Handle(pattern string, handler http.Handler) { rt.mux.Handle(pattern, handler) } func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { var withDebug bool if msg := rt.log.Debug(); msg.Enabled() { withDebug = true w = &traceResponseWriter{original: w} msg.Str("method", r.Method).Str("uri", r.RequestURI).HTTPIP(r).Msg("ServeHTTP") } if prefixLen := len(rt.urlPrefix); prefixLen > 1 { if len(r.URL.Path) < prefixLen || r.URL.Path[:prefixLen] != rt.urlPrefix { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) if withDebug { rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("/ServeHTTP/prefix") } return } r.URL.Path = r.URL.Path[prefixLen-1:] } r.Body = http.MaxBytesReader(w, r.Body, rt.maxReqSize) match := rt.reURL.FindStringSubmatch(r.URL.Path) if len(match) != 3 { rt.mux.ServeHTTP(w, rt.addUserContext(r)) if withDebug { rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("match other") } return } if withDebug { rt.log.Debug().Str("key", match[1]).Str("zid", match[2]).Msg("path match") } key := match[1][0] var mh *methodHandler if match[2] == "" { mh = rt.listTable[key] } else { mh = rt.zettelTable[key] } method, ok := mapMethod[r.Method] if ok && mh != nil { if handler := mh[method]; handler != nil { r.URL.Path = "/" + match[2] handler.ServeHTTP(w, rt.addUserContext(r)) if withDebug { rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("/ServeHTTP") } return } } http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) if withDebug { rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("no match") } } func (rt *httpRouter) addUserContext(r *http.Request) *http.Request { if rt.ur == nil { // No auth needed return r } k := auth.KindAPI t := getHeaderToken(r) if len(t) == 0 { rt.log.Debug().Msg("no jwt token found") // IP already logged: ServeHTTP k = auth.KindwebUI t = getSessionToken(r) } if len(t) == 0 { rt.log.Debug().Msg("no auth token found in request") // IP already logged: ServeHTTP return r } tokenData, err := rt.auth.CheckToken(t, k) if err != nil { rt.log.Info().Err(err).HTTPIP(r).Msg("invalid auth token") return r } ctx := r.Context() user, err := rt.ur.GetUser(ctx, tokenData.Zid, tokenData.Ident) if err != nil { rt.log.Info().Zid(tokenData.Zid).Str("ident", tokenData.Ident).Err(err).HTTPIP(r).Msg("auth user not found") return r } return r.WithContext(updateContext(ctx, user, &tokenData)) } func getSessionToken(r *http.Request) []byte { cookie, err := r.Cookie(sessionName) if err != nil { return nil } return []byte(cookie.Value) } func getHeaderToken(r *http.Request) []byte { h := r.Header["Authorization"] if h == nil { return nil } // “Multiple message-header fields with the same field-name MAY be // present in a message if and only if the entire field-value for that // header field is defined as a comma-separated list.” // — “Hypertext Transfer Protocol” RFC 2616, subsection 4.2 auth := strings.Join(h, ", ") const prefix = "Bearer " // RFC 2617, subsection 1.2 defines the scheme token as case-insensitive. if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) { return nil } return []byte(auth[len(prefix):]) } type traceResponseWriter struct { original http.ResponseWriter statusCode int } func (w *traceResponseWriter) Header() http.Header { return w.original.Header() } func (w *traceResponseWriter) Write(p []byte) (int, error) { return w.original.Write(p) } func (w *traceResponseWriter) WriteHeader(statusCode int) { w.statusCode = statusCode w.original.WriteHeader(statusCode) } func (w *traceResponseWriter) WriteString(s string) (int, error) { return io.WriteString(w.original, s) } |
Added internal/web/server/server.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package server provides the Zettelstore web service. package server import ( "context" "net/http" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" ) // UserRetriever allows to retrieve user data based on a given zettel identifier. type UserRetriever interface { GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) } // Method enumerates the allowed HTTP methods. type Method uint8 // Values for method type const ( MethodGet Method = iota MethodHead MethodPost MethodPut MethodDelete methodLAST // must always be the last one ) // Router allows to state routes for various URL paths. type Router interface { Handle(pattern string, handler http.Handler) AddListRoute(key byte, method Method, handler http.Handler) AddZettelRoute(key byte, method Method, handler http.Handler) SetUserRetriever(ur UserRetriever) } // Builder allows to build new URLs for the web service. type Builder interface { GetURLPrefix() string NewURLBuilder(key byte) *api.URLBuilder NewURLBuilderAbs(key byte) *api.URLBuilder } // Auth is the authencation interface. type Auth interface { // SetToken sends the token to the client. SetToken(w http.ResponseWriter, token []byte, d time.Duration) // ClearToken invalidates the session cookie by sending an empty one. ClearToken(ctx context.Context, w http.ResponseWriter) context.Context } // AuthData stores all relevant authentication data for a context. type AuthData struct { User *meta.Meta Token []byte Now time.Time Issued time.Time Expires time.Time } // GetAuthData returns the full authentication data from the context. func GetAuthData(ctx context.Context) *AuthData { if ctx != nil { data, ok := ctx.Value(ctxKeySession).(*AuthData) if ok { return data } } return nil } // GetUser returns the metadata of the current user, or nil if there is no one. func GetUser(ctx context.Context) *meta.Meta { if data := GetAuthData(ctx); data != nil { return data.User } return nil } // ctxKeyTypeSession is just an additional type to make context value retrieval unambiguous. type ctxKeyTypeSession struct{} // ctxKeySession is the key value to retrieve Authdata var ctxKeySession ctxKeyTypeSession // AuthBuilder is a Builder that also allows to execute authentication functions. type AuthBuilder interface { Auth Builder } // Server is the main web server for accessing Zettelstore via HTTP. type Server interface { Router Auth Builder Run() error Stop() } |
Added internal/zettel/content.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package zettel import ( "bytes" "encoding/base64" "errors" "io" "unicode" "unicode/utf8" "t73f.de/r/zsx/input" ) // Content is just the content of a zettel. type Content struct { data []byte isBinary bool } // NewContent creates a new content from a string. func NewContent(data []byte) Content { return Content{data: data, isBinary: IsBinary(data)} } // Length returns the number of bytes stored. func (zc *Content) Length() int { return len(zc.data) } // Equal compares two content values. func (zc *Content) Equal(o *Content) bool { if zc == nil { return o == nil } if zc.isBinary != o.isBinary { return false } return bytes.Equal(zc.data, o.data) } // Write it to a Writer func (zc *Content) Write(w io.Writer) (int, error) { return w.Write(zc.data) } // AsString returns the content itself is a string. func (zc *Content) AsString() string { return string(zc.data) } // AsBytes returns the content itself is a byte slice. func (zc *Content) AsBytes() []byte { return zc.data } // IsBinary returns true if the content contains non-unicode values or is, // interpreted a text, with a high probability binary content. func (zc *Content) IsBinary() bool { return zc.isBinary } // TrimSpace remove some space character in content, if it is not binary content. func (zc *Content) TrimSpace() { if zc.isBinary { return } inp := input.NewInput(zc.data) pos := inp.Pos for inp.Ch != input.EOS { if input.IsEOLEOS(inp.Ch) { inp.Next() pos = inp.Pos continue } if !inp.IsSpace() { break } inp.Next() } zc.data = bytes.TrimRightFunc(inp.Src[pos:], unicode.IsSpace) } // Encode content for future transmission. func (zc *Content) Encode() (data, encoding string) { if !zc.isBinary { return zc.AsString(), "" } return base64.StdEncoding.EncodeToString(zc.data), "base64" } // SetDecoded content to the decoded value of the given string. func (zc *Content) SetDecoded(data, encoding string) error { switch encoding { case "": zc.data = []byte(data) case "base64": decoded, err := base64.StdEncoding.DecodeString(data) if err != nil { return err } zc.data = decoded default: return errors.New("unknown encoding " + encoding) } zc.isBinary = IsBinary(zc.data) return nil } // IsBinary returns true if the given data appears to be non-text data. func IsBinary(data []byte) bool { if !utf8.Valid(data) { return true } for i := range len(data) { if data[i] == 0 { return true } } return false } |
Added internal/zettel/content_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package zettel_test import ( "testing" "zettelstore.de/z/internal/zettel" ) func TestContentIsBinary(t *testing.T) { t.Parallel() td := []struct { s string exp bool }{ {"abc", false}, {"äöü", false}, {"", false}, {string([]byte{0}), true}, } for i, tc := range td { content := zettel.NewContent([]byte(tc.s)) got := content.IsBinary() if got != tc.exp { t.Errorf("TC=%d: expected %v, got %v", i, tc.exp, got) } } } func TestTrimSpace(t *testing.T) { t.Parallel() testcases := []struct { in, exp string }{ {"", ""}, {" ", ""}, {"abc", "abc"}, {" abc", " abc"}, {"abc ", "abc"}, {"abc \n", "abc"}, {"abc\n ", "abc"}, {"\nabc", "abc"}, {" \nabc", "abc"}, {" \n abc", " abc"}, {" \n\n abc", " abc"}, {" \n \n abc", " abc"}, {" \n \n abc \n \n ", " abc"}, } for _, tc := range testcases { c := zettel.NewContent([]byte(tc.in)) c.TrimSpace() got := c.AsString() if got != tc.exp { t.Errorf("TrimSpace(%q) should be %q, but got %q", tc.in, tc.exp, got) } } } |
Added internal/zettel/zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package zettel provides specific types, constants, and functions for zettel. package zettel import "t73f.de/r/zsc/domain/meta" // Zettel is the main data object of a zettelstore. type Zettel struct { Meta *meta.Meta // Some additional meta-data. Content Content // The content of the zettel itself. } // ByteSize returns the number of bytes to store the zettel (in a zettel view, // not in a technical view). func (z Zettel) ByteSize() int { return z.Meta.ByteSize() + z.Content.Length() } // Equal compares two zettel for equality. func (z Zettel) Equal(o Zettel, allowComputed bool) bool { return z.Meta.Equal(o.Meta, allowComputed) && z.Content.Equal(&o.Content) } |
Deleted kernel/impl/auth.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/cfg.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/cmd.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/config.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/core.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/impl.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/log.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/server.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/web.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/kernel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted logger/logger.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted logger/logger_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted logger/message.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/blob/blob.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/cleaner/cleaner.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/ORIG_CONTRIBUTORS.
|
| < < < |
Deleted parser/draw/ORIG_LICENSE.
|
| < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/canvas.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/canvas_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/char.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/draw.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/draw_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/object.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/point.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/svg.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/svg_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/markdown/markdown.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/none/none.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/parser.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/parser_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/plain/plain.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/plain/plain_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/zettelmark/zettelmark.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/zettelmark/zettelmark_fuzz_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/zettelmark/zettelmark_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/compiled.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/context.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/parser.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/parser_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/print.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/query.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/retrieve.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/select.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/select_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/sorter.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/specs.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/unlinked.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted strfun/escape.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to tests/client/client_test.go.
︙ | ︙ | |||
25 26 27 28 29 30 31 | "strconv" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/client" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" | | | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | "strconv" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/client" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/kernel" ) func nextZid(zid id.Zid) id.Zid { return zid + 1 } func TestNextZid(t *testing.T) { testCases := []struct { zid, exp id.Zid |
︙ | ︙ |
Changes to tests/markdown_test.go.
︙ | ︙ | |||
19 20 21 22 23 24 25 | "fmt" "os" "strings" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" | | > | | | < < < < < < | < < | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | "fmt" "os" "strings" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/parser" ) type markdownTestCase struct { Markdown string `json:"markdown"` HTML string `json:"html"` Example int `json:"example"` StartLine int `json:"start_line"` |
︙ | ︙ | |||
77 78 79 80 81 82 83 | ast := createMDBlockSlice(tc.Markdown, config.NoHTML) testAllEncodings(t, tc, &ast) testZmkEncoding(t, tc, &ast) } } func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice { | | | 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | ast := createMDBlockSlice(tc.Markdown, config.NoHTML) testAllEncodings(t, tc, &ast) testZmkEncoding(t, tc, &ast) } } func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice { return parser.Parse(input.NewInput([]byte(markdown)), nil, meta.ValueSyntaxMarkdown, hi) } func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { var sb strings.Builder testID := tc.Example*100 + 1 for _, enc := range encodings { t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(*testing.T) { |
︙ | ︙ | |||
101 102 103 104 105 106 107 | testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { buf.Reset() zmkEncoder.WriteBlocks(&buf, ast) // gotFirst := buf.String() testID = tc.Example*100 + 2 | | | | 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { buf.Reset() zmkEncoder.WriteBlocks(&buf, ast) // gotFirst := buf.String() testID = tc.Example*100 + 2 secondAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML) buf.Reset() zmkEncoder.WriteBlocks(&buf, &secondAst) gotSecond := buf.String() // if gotFirst != gotSecond { // st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond) // } testID = tc.Example*100 + 3 thirdAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML) buf.Reset() zmkEncoder.WriteBlocks(&buf, &thirdAst) gotThird := buf.String() if gotSecond != gotThird { st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird) } |
︙ | ︙ |
Changes to tests/naughtystrings_test.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | "bufio" "io" "os" "path/filepath" "testing" "t73f.de/r/zsc/domain/meta" | | > | | | > > | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | "bufio" "io" "os" "path/filepath" "testing" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/parser" _ "zettelstore.de/z/cmd" ) // Test all parser / encoder with a list of "naughty strings", i.e. unusual strings // that often crash software. func getNaughtyStrings() (result []string, err error) { fpath := filepath.Join("..", "testdata", "naughty", "blns.txt") |
︙ | ︙ | |||
78 79 80 81 82 83 84 | } encs := getAllEncoder() if len(encs) == 0 { t.Fatal("no encoder found") } for _, s := range blns { for _, pinfo := range pinfos { | | | 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | } encs := getAllEncoder() if len(encs) == 0 { t.Fatal("no encoder found") } for _, s := range blns { for _, pinfo := range pinfos { bs := parser.Parse(input.NewInput([]byte(s)), &meta.Meta{}, pinfo.Name, config.NoHTML) for _, enc := range encs { _, err = enc.WriteBlocks(io.Discard, &bs) if err != nil { t.Error(err) } } } } } |
Changes to tests/regression_test.go.
︙ | ︙ | |||
22 23 24 25 26 27 28 | "os" "path/filepath" "strings" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" | > | | | | | | | | | | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | "os" "path/filepath" "strings" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/query" _ "zettelstore.de/z/internal/box/dirbox" ) var encodings = []api.EncodingEnum{ api.EncoderHTML, api.EncoderSz, api.EncoderText, } |
︙ | ︙ |
Changes to tools/build/build.go.
︙ | ︙ | |||
25 26 27 28 29 30 31 | "path/filepath" "slices" "strings" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" | | | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | "path/filepath" "slices" "strings" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/strfun" "zettelstore.de/z/tools" ) func readVersionFile() (string, error) { content, err := os.ReadFile("VERSION") if err != nil { |
︙ | ︙ |
Deleted usecase/authenticate.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/create_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/delete_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/evaluate.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/get_all_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/get_special_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/get_user.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/get_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/lists.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/parse_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/query.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/refresh.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/reindex.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/update_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/usecase.go.
|
| < < < < < < < < < < < < < < < |
Deleted usecase/version.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/adapter.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/api.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/command.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/create_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/delete_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/get_data.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/get_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/login.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/query.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/request.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/response.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/update_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/errors.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/request.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/response.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/const.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/create_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/delete_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/edit_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/favicon.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/forms.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/forms_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/get_info.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/get_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/goaction.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/home.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/htmlgen.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/htmlmeta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/lists.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/login.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/meta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/meta_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/response.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/sxn_code.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/template.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/webui.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/content/content.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/content/content_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/server/impl/http.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/server/impl/impl.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/server/impl/router.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/server/server.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to www/changes.wiki.
1 2 3 | <title>Change Log</title> <a id="0_20"></a> | > > > > > > > > > > > > > > > | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | <title>Change Log</title> <a id="0_21"></a> <h2>Changes for Version 0.21.0 (pending)</h2> * Sz encodings of links were simplified into one <code>LINK</code> symbol. Different link types are now specified by reference node. (breaking: api) * Sz encodings of lists, descriptions, tables, table cells, and block BLOBs have now an additional attributes entry, directly after the initial symbol. (breaking: api) * Sz encoding for table cell alignment (<code>CELL-*</code>) is now done via table cell attribute <code>(align . *)</code>, where <code>*</code> is the alignment value (<code>"center"</code>, <code>"left"</code>, or <code>"right"</code>). (breaking: api) <a id="0_20"></a> <h2>Changes for Version 0.20.0 (2025-03-07)</h2> * Metadata with keys that have the suffix <code>-title</code> are no longer interpreted as Zettelmarkup. This was a leftover from [#0_11|v0.11], when type of metadata <code>title</code> was changed from Zettelmarkup to a possibly empty string. (breaking) * Type of metadata key <code>summary</code> changed from Zettelmarkup to plain text. You might need to update your zettel that contain this key. (breaking) * Remove support for metadata type <code>Zettelmarkup</code>. It made implementation too complex, and it was seldom used (see above). (breaking) * Remove metadata key <code>created-missing</code>, which was used to support the cancelled migration process into a four letter zettel identifier format. (breaking) * Remove support for inline zettel snippets. Mostly used for some HTML elements, which hinders portability and encoding in other formats. (breaking: zettelmarkup) * Remove support for inline HTML text within Markdown text. Such HTML code (did I ever say that Markdown is just a super-set of HTML?) is now translated into literal text. (breaking: markdown/CommonMark) * Query aggregates <code>ATOM</code> and <code>RSS</code> are removed, as well as the accompanying <code>TITLE</code> query action (parameter). Was announced as deprecated in version 0.19. (breaking) * Query data encoding and aggregate data encoding were changed to remove the superfluous <code>(list ...)</code>. Instead, the result list is now spliced into the returned s-expression list. If you use the Zettelstore Client, it will handle that for you. (breaking: api) * “Lists” menu is build by reading a zettel with menu items |
︙ | ︙ |
Changes to www/download.wiki.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it. <h2>ZIP-ped Executables</h2> Build: <code>v0.20.0</code> (2025-03-07). * [/uv/zettelstore-0.20.0-linux-amd64.zip|Linux] (amd64) * [/uv/zettelstore-0.20.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore-0.20.0-darwin-arm64.zip|macOS] (arm64) * [/uv/zettelstore-0.20.0-darwin-amd64.zip|macOS] (amd64) * [/uv/zettelstore-0.20.0-windows-amd64.zip|Windows] (amd64) Unzip the appropriate file, install and execute Zettelstore according to the manual. | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it. <h2>ZIP-ped Executables</h2> Build: <code>v0.20.0</code> (2025-03-07). * [/uv/zettelstore-0.20.0-android-arm64.zip|Android] (arm64) * [/uv/zettelstore-0.20.0-linux-amd64.zip|Linux] (amd64) * [/uv/zettelstore-0.20.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore-0.20.0-darwin-arm64.zip|macOS] (arm64) * [/uv/zettelstore-0.20.0-darwin-amd64.zip|macOS] (amd64) * [/uv/zettelstore-0.20.0-windows-amd64.zip|Windows] (amd64) Unzip the appropriate file, install and execute Zettelstore according to the manual. |
︙ | ︙ |
Deleted zettel/content.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/content_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |