DEF
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+
test
+`"'>
+`"'>
+`"'>
+`"'>
+`"'>
+`"'>
+`"'>
+`"'>
+`"'>
+`"'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+"`'>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
XXX
+
+
+
+
+
+
+
+
+ ">
+
+
+
+
+
+
+
+
+
+
+
+perl -e 'print " ";' > out
+
+
+
+
+<
+
+
+# SQL Injection
+#
+# Strings which can cause a SQL injection if inputs are not sanitized
+
+1;DROP TABLE users
+1'; DROP TABLE users-- 1
+' OR 1=1 -- 1
+' OR '1'='1
+'; EXEC sp_MSForEachTable 'DROP TABLE ?'; --
+
+%
+_
+
+# Server Code Injection
+#
+# Strings which can cause user to run code on server as a privileged user (c.f. https://news.ycombinator.com/item?id=7665153)
+
+-
+--
+--version
+--help
+$USER
+/dev/null; touch /tmp/blns.fail ; echo
+`touch /tmp/blns.fail`
+$(touch /tmp/blns.fail)
+@{[system "touch /tmp/blns.fail"]}
+
+# Command Injection (Ruby)
+#
+# Strings which can call system commands within Ruby/Rails applications
+
+eval("puts 'hello world'")
+System("ls -al /")
+`ls -al /`
+Kernel.exec("ls -al /")
+Kernel.exit(1)
+%x('ls -al /')
+
+# XXE Injection (XML)
+#
+# String which can reveal system files when parsed by a badly configured XML parser
+
+]>
&xxe;
+
+# Unwanted Interpolation
+#
+# Strings which can be accidentally expanded into different strings if evaluated in the wrong context, e.g. used as a printf format string or via Perl or shell eval. Might expose sensitive data from the program doing the interpolation, or might just represent the wrong string.
+
+$HOME
+$ENV{'HOME'}
+%d
+%s%s%s%s%s
+{0}
+%*.*s
+%@
+%n
+File:///
+
+# File Inclusion
+#
+# Strings which can cause user to pull in files that should not be a part of a web server
+
+../../../../../../../../../../../etc/passwd%00
+../../../../../../../../../../../etc/hosts
+
+# Known CVEs and Vulnerabilities
+#
+# Strings that test for known vulnerabilities
+
+() { 0; }; touch /tmp/blns.shellshock1.fail;
+() { _; } >_[$($())] { touch /tmp/blns.shellshock2.fail; }
+<<< %s(un='%s') = %u
++++ATH0
+
+# MSDOS/Windows Special Filenames
+#
+# Strings which are reserved characters in MSDOS/Windows
+
+CON
+PRN
+AUX
+CLOCK$
+NUL
+A:
+ZZ:
+COM1
+LPT1
+LPT2
+LPT3
+COM2
+COM3
+COM4
+
+# IRC specific strings
+#
+# Strings that may occur on IRC clients that make security products freak out
+
+DCC SEND STARTKEYLOGGER 0 0 0
+
+# Scunthorpe Problem
+#
+# Innocuous strings which may be blocked by profanity filters (https://en.wikipedia.org/wiki/Scunthorpe_problem)
+
+Scunthorpe General Hospital
+Penistone Community Church
+Lightwater Country Park
+Jimmy Clitheroe
+Horniman Museum
+shitake mushrooms
+RomansInSussex.co.uk
+http://www.cum.qc.ca/
+Craig Cockburn, Software Specialist
+Linda Callahan
+Dr. Herman I. Libshitz
+magna cum laude
+Super Bowl XXX
+medieval erection of parapets
+evaluate
+mocha
+expression
+Arsenal canal
+classic
+Tyson Gay
+Dick Van Dyke
+basement
+
+# Human injection
+#
+# Strings which may cause human to reinterpret worldview
+
+If you're reading this, you've been in a coma for almost 20 years now. We're trying a new technique. We don't know where this message will end up in your dream, but we hope it works. Please wake up, we miss you.
+
+# Terminal escape codes
+#
+# Strings which punish the fools who use cat/type on this file
+
+Roses are [0;31mred[0m, violets are [0;34mblue. Hope you enjoy terminal hue
+But now...[20Cfor my greatest trick...[8m
+The quick brown fox... [Beeeep]
+
+# iOS Vulnerabilities
+#
+# Strings which crashed iMessage in various versions of iOS
+
+Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗
+🏳0🌈️
+జ్ఞా
+
+# Persian special characters
+#
+# This is a four characters string which includes Persian special characters (گچپژ)
+
+گچپژ
+
+# jinja2 injection
+#
+# first one is supposed to raise "MemoryError" exception
+# second, obviously, prints contents of /etc/passwd
+
+{% print 'x' * 64 * 1024**3 %}
+{{ "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read() }}
ADDED testdata/testbox/20230929102100.zettel
Index: testdata/testbox/20230929102100.zettel
==================================================================
--- testdata/testbox/20230929102100.zettel
+++ testdata/testbox/20230929102100.zettel
@@ -0,0 +1,7 @@
+id: 20230929102100
+title: #test
+role: tag
+syntax: zmk
+created: 20230929102125
+
+Zettel with this tag are testing the Zettelstore.
Index: tests/client/client_test.go
==================================================================
--- tests/client/client_test.go
+++ tests/client/client_test.go
@@ -1,29 +1,34 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 Detlef Stern
+// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore client 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 client provides a client for accessing the Zettelstore via its API.
package client_test
import (
"context"
"flag"
"fmt"
+ "io"
"net/http"
"net/url"
+ "slices"
"strconv"
"testing"
- "zettelstore.de/c/api"
- "zettelstore.de/c/client"
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/client"
"zettelstore.de/z/kernel"
)
func nextZid(zid api.ZettelID) api.ZettelID {
numVal, err := strconv.ParseUint(string(zid), 10, 64)
@@ -47,16 +52,16 @@
}
}
func TestListZettel(t *testing.T) {
const (
- ownerZettel = 49
- configRoleZettel = 31
+ ownerZettel = 56
+ configRoleZettel = 34
writerZettel = ownerZettel - 25
readerZettel = ownerZettel - 25
- creatorZettel = 7
- publicZettel = 4
+ creatorZettel = 10
+ publicZettel = 5
)
testdata := []struct {
user string
exp int
@@ -68,15 +73,14 @@
{"owner", ownerZettel},
}
t.Parallel()
c := getClient()
- query := url.Values{api.QueryKeyEncoding: {api.EncodingHTML}} // Client must remove "html"
for i, tc := range testdata {
t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) {
c.SetAuth(tc.user, tc.user)
- q, h, l, err := c.ListZettelJSON(context.Background(), query)
+ q, h, l, err := c.QueryZettelData(context.Background(), "")
if err != nil {
tt.Error(err)
return
}
if q != "" {
@@ -89,54 +93,55 @@
if got != tc.exp {
tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l)
}
})
}
- q, h, l, err := c.ListZettelJSON(context.Background(), url.Values{api.KeyRole: {api.ValueRoleConfiguration}})
+ search := api.KeyRole + api.SearchOperatorHas + api.ValueRoleConfiguration + " ORDER id"
+ q, h, l, err := c.QueryZettelData(context.Background(), search)
if err != nil {
t.Error(err)
return
}
- expQ := "role:configuration"
+ expQ := "role:configuration ORDER id"
if q != expQ {
t.Errorf("Query should be %q, but is %q", expQ, q)
}
- expH := "role MATCH configuration"
+ expH := "role HAS configuration ORDER id"
if h != expH {
t.Errorf("Human should be %q, but is %q", expH, h)
}
got := len(l)
if got != configRoleZettel {
t.Errorf("List of length %d expected, but got %d\n%v", configRoleZettel, got, l)
}
- pl, err := c.ListZettel(context.Background(), url.Values{api.KeyRole: {api.ValueRoleConfiguration}})
+ pl, err := c.QueryZettel(context.Background(), search)
if err != nil {
t.Error(err)
return
}
compareZettelList(t, pl, l)
}
-func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaJSON) {
+func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaRights) {
t.Helper()
if len(pl) != len(l) {
- t.Errorf("Different list lenght: Plain=%d, JSON=%d", len(pl), len(l))
+ t.Errorf("Different list lenght: Plain=%d, Data=%d", len(pl), len(l))
} else {
for i, line := range pl {
if got := api.ZettelID(line[:14]); got != l[i].ID {
- t.Errorf("%d: JSON=%q, got=%q", i, l[i].ID, got)
+ t.Errorf("%d: Data=%q, got=%q", i, l[i].ID, got)
}
}
}
}
-func TestGetZettelJSON(t *testing.T) {
+func TestGetZettelData(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
- z, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome)
+ z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
if err != nil {
t.Error(err)
return
}
if m := z.Meta; len(m) == 0 {
@@ -144,21 +149,24 @@
}
if z.Content == "" || z.Encoding != "" {
t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding)
}
- m, err := c.GetMeta(context.Background(), api.ZidDefaultHome)
+ mr, err := c.GetMetaData(context.Background(), api.ZidDefaultHome)
if err != nil {
t.Error(err)
return
}
- if len(m) != len(z.Meta) {
- t.Errorf("Pure meta differs from zettel meta: %s vs %s", m, z.Meta)
+ if mr.Rights == api.ZettelCanNone {
+ t.Error("rights must be greater zero")
+ }
+ if len(mr.Meta) != len(z.Meta) {
+ t.Errorf("Pure meta differs from zettel meta: %s vs %s", mr.Meta, z.Meta)
return
}
for k, v := range z.Meta {
- got, ok := m[k]
+ got, ok := mr.Meta[k]
if !ok {
t.Errorf("Pure meta has no key %q", k)
continue
}
if got != v {
@@ -170,13 +178,12 @@
func TestGetParsedEvaluatedZettel(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
encodings := []api.EncodingEnum{
- api.EncoderZJSON,
api.EncoderHTML,
- api.EncoderSexpr,
+ api.EncoderSz,
api.EncoderText,
}
for _, enc := range encodings {
content, err := c.GetParsedZettel(context.Background(), api.ZidDefaultHome, enc)
if err != nil {
@@ -195,20 +202,11 @@
t.Errorf("Empty content for evaluated encoding %v", enc)
}
}
}
-func checkZid(t *testing.T, expected, got api.ZettelID) bool {
- t.Helper()
- if expected != got {
- t.Errorf("Expected a Zid %q, but got %q", expected, got)
- return false
- }
- return true
-}
-
-func checkListZid(t *testing.T, l []api.ZidMetaJSON, pos int, expected api.ZettelID) {
+func checkListZid(t *testing.T, l []api.ZidMetaRights, pos int, expected api.ZettelID) {
t.Helper()
if got := api.ZettelID(l[pos].ID); got != expected {
t.Errorf("Expected result[%d]=%v, but got %v", pos, expected, got)
}
}
@@ -215,89 +213,83 @@
func TestGetZettelOrder(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
- rl, err := c.GetZettelOrder(context.Background(), api.ZidTOCNewTemplate)
- if err != nil {
- t.Error(err)
- return
- }
- if !checkZid(t, api.ZidTOCNewTemplate, rl.ID) {
- return
- }
- l := rl.List
- if got := len(l); got != 2 {
- t.Errorf("Expected list of length 2, got %d", got)
- return
- }
- checkListZid(t, l, 0, api.ZidTemplateNewZettel)
- checkListZid(t, l, 1, api.ZidTemplateNewUser)
-}
-
-func TestGetZettelContext(t *testing.T) {
- const (
- allUserZid = api.ZettelID("20211019200500")
- ownerZid = api.ZettelID("20210629163300")
- writerZid = api.ZettelID("20210629165000")
- readerZid = api.ZettelID("20210629165024")
- creatorZid = api.ZettelID("20210629165050")
- limitAll = 3
- )
- t.Parallel()
- c := getClient()
- c.SetAuth("owner", "owner")
- rl, err := c.GetZettelContext(context.Background(), ownerZid, client.DirBoth, 0, limitAll)
- if err != nil {
- t.Error(err)
- return
- }
- if !checkZid(t, ownerZid, rl.ID) {
- return
- }
- l := rl.List
- if got := len(l); got != limitAll {
- t.Errorf("Expected list of length %d, got %d", limitAll, got)
- t.Error(rl)
- return
- }
- checkListZid(t, l, 0, allUserZid)
- checkListZid(t, l, 1, writerZid)
- checkListZid(t, l, 2, readerZid)
- // checkListZid(t, l, 3, creatorZid)
-
- rl, err = c.GetZettelContext(context.Background(), ownerZid, client.DirBackward, 0, 0)
- if err != nil {
- t.Error(err)
- return
- }
- if !checkZid(t, ownerZid, rl.ID) {
- return
- }
- l = rl.List
- if got := len(l); got != 1 {
- t.Errorf("Expected list of length 1, got %d", got)
- return
- }
- checkListZid(t, l, 0, allUserZid)
-}
+ _, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidTOCNewTemplate)+" "+api.ItemsDirective)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if got := len(metaSeq); got != 4 {
+ t.Errorf("Expected list of length 4, got %d", got)
+ return
+ }
+ checkListZid(t, metaSeq, 0, api.ZidTemplateNewZettel)
+ checkListZid(t, metaSeq, 1, api.ZidTemplateNewRole)
+ checkListZid(t, metaSeq, 2, api.ZidTemplateNewTag)
+ checkListZid(t, metaSeq, 3, api.ZidTemplateNewUser)
+}
+
+// func TestGetZettelContext(t *testing.T) {
+// const (
+// allUserZid = api.ZettelID("20211019200500")
+// ownerZid = api.ZettelID("20210629163300")
+// writerZid = api.ZettelID("20210629165000")
+// readerZid = api.ZettelID("20210629165024")
+// creatorZid = api.ZettelID("20210629165050")
+// limitAll = 3
+// )
+// t.Parallel()
+// c := getClient()
+// c.SetAuth("owner", "owner")
+// rl, err := c.GetZettelContext(context.Background(), ownerZid, client.DirBoth, 0, limitAll)
+// if err != nil {
+// t.Error(err)
+// return
+// }
+// if !checkZid(t, ownerZid, rl.ID) {
+// return
+// }
+// l := rl.List
+// if got := len(l); got != limitAll {
+// t.Errorf("Expected list of length %d, got %d", limitAll, got)
+// t.Error(rl)
+// return
+// }
+// checkListZid(t, l, 0, allUserZid)
+// // checkListZid(t, l, 1, writerZid)
+// // checkListZid(t, l, 2, readerZid)
+// checkListZid(t, l, 1, creatorZid)
+
+// rl, err = c.GetZettelContext(context.Background(), ownerZid, client.DirBackward, 0, 0)
+// if err != nil {
+// t.Error(err)
+// return
+// }
+// if !checkZid(t, ownerZid, rl.ID) {
+// return
+// }
+// l = rl.List
+// if got, exp := len(l), 4; got != exp {
+// t.Errorf("Expected list of length %d, got %d", exp, got)
+// return
+// }
+// checkListZid(t, l, 0, allUserZid)
+// }
func TestGetUnlinkedReferences(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
- zl, err := c.GetUnlinkedReferences(context.Background(), api.ZidDefaultHome, nil)
+ _, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidDefaultHome)+" "+api.UnlinkedDirective)
if err != nil {
t.Error(err)
return
}
- if !checkZid(t, api.ZidDefaultHome, zl.ID) {
- return
- }
- l := zl.List
- if got := len(l); got != 1 {
- t.Errorf("Expected list of length 1, got %d", got)
+ if got := len(metaSeq); got != 1 {
+ t.Errorf("Expected list of length 1, got %d:\n%v", got, metaSeq)
return
}
}
func failNoErrorOrNoCode(t *testing.T, err error, goodCode int) bool {
@@ -338,11 +330,11 @@
func TestListTags(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
- tm, err := c.ListMapMeta(context.Background(), api.KeyTags)
+ agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyTags)
if err != nil {
t.Error(err)
return
}
tags := []struct {
@@ -351,51 +343,128 @@
}{
{"#invisible", 1},
{"#user", 4},
{"#test", 4},
}
- if len(tm) != len(tags) {
- t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(tm), tm)
+ if len(agg) != len(tags) {
+ t.Errorf("Expected %d different tags, but got %d (%v)", len(tags), len(agg), agg)
}
for _, tag := range tags {
- if zl, ok := tm[tag.key]; !ok {
- t.Errorf("No tag %v: %v", tag.key, tm)
+ if zl, ok := agg[tag.key]; !ok {
+ t.Errorf("No tag %v: %v", tag.key, agg)
} else if len(zl) != tag.size {
t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl)
}
}
- for i, id := range tm["#user"] {
- if id != tm["#test"][i] {
- t.Errorf("Tags #user and #test have different content: %v vs %v", tm["#user"], tm["#test"])
+ for i, id := range agg["#user"] {
+ if id != agg["#test"][i] {
+ t.Errorf("Tags #user and #test have different content: %v vs %v", agg["#user"], agg["#test"])
}
}
}
+
+func TestTagZettel(t *testing.T) {
+ t.Parallel()
+ c := getClient()
+ c.AllowRedirect(true)
+ c.SetAuth("owner", "owner")
+ ctx := context.Background()
+ zid, err := c.TagZettel(ctx, "nosuchtag")
+ if err != nil {
+ t.Error(err)
+ } else if zid != "" {
+ t.Errorf("no zid expected, but got %q", zid)
+ }
+ zid, err = c.TagZettel(ctx, "#test")
+ exp := api.ZettelID("20230929102100")
+ if err != nil {
+ t.Error(err)
+ } else if zid != exp {
+ t.Errorf("tag zettel for #test should be %q, but got %q", exp, zid)
+ }
+}
func TestListRoles(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
- rl, err := c.ListMapMeta(context.Background(), api.KeyRole)
+ agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyRole)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ exp := []string{"configuration", "role", "user", "tag", "zettel"}
+ if len(agg) != len(exp) {
+ t.Errorf("Expected %d different roles, but got %d (%v)", len(exp), len(agg), agg)
+ }
+ for _, id := range exp {
+ if _, found := agg[id]; !found {
+ t.Errorf("Role map expected key %q", id)
+ }
+ }
+}
+
+func TestRoleZettel(t *testing.T) {
+ t.Parallel()
+ c := getClient()
+ c.AllowRedirect(true)
+ c.SetAuth("owner", "owner")
+ ctx := context.Background()
+ zid, err := c.RoleZettel(ctx, "nosuchrole")
+ if err != nil {
+ t.Error("AAA", err)
+ } else if zid != "" {
+ t.Errorf("no zid expected, but got %q", zid)
+ }
+ zid, err = c.RoleZettel(ctx, "zettel")
+ exp := api.ZettelID("00000000060010")
+ if err != nil {
+ t.Error(err)
+ } else if zid != exp {
+ t.Errorf("role zettel for zettel should be %q, but got %q", exp, zid)
+ }
+}
+
+func TestRedirect(t *testing.T) {
+ t.Parallel()
+ c := getClient()
+ search := api.OrderDirective + " " + api.ReverseDirective + " " + api.KeyID + api.ActionSeparator + api.RedirectAction
+ ub := c.NewURLBuilder('z').AppendQuery(search)
+ respRedirect, err := http.Get(ub.String())
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer respRedirect.Body.Close()
+ bodyRedirect, err := io.ReadAll(respRedirect.Body)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ ub.ClearQuery().SetZid(api.ZidEmoji)
+ respEmoji, err := http.Get(ub.String())
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer respEmoji.Body.Close()
+ bodyEmoji, err := io.ReadAll(respEmoji.Body)
if err != nil {
t.Error(err)
return
}
- exp := []string{"configuration", "user", "zettel"}
- if len(rl) != len(exp) {
- t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(rl), rl)
- }
- for _, id := range exp {
- if _, found := rl[id]; !found {
- t.Errorf("Role map expected key %q", id)
- }
+ if !slices.Equal(bodyRedirect, bodyEmoji) {
+ t.Error("Wrong redirect")
+ t.Error("REDIRECT", respRedirect)
+ t.Error("EXPECTED", respEmoji)
}
}
func TestVersion(t *testing.T) {
t.Parallel()
c := getClient()
- ver, err := c.GetVersionJSON(context.Background())
+ ver, err := c.GetVersionInfo(context.Background())
if err != nil {
t.Error(err)
return
}
if ver.Major != -1 || ver.Minor != -1 || ver.Patch != -1 || ver.Info != kernel.CoreDefaultVersion || ver.Hash != "" {
Index: tests/client/crud_test.go
==================================================================
--- tests/client/crud_test.go
+++ tests/client/crud_test.go
@@ -1,24 +1,27 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 Detlef Stern
+// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore client 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 client_test
import (
"context"
"strings"
"testing"
- "zettelstore.de/c/api"
- "zettelstore.de/c/client"
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/client"
)
// ---------------------------------------------------------------------------
// Tests that change the Zettelstore must nor run parallel to other tests.
@@ -57,15 +60,15 @@
}
doDelete(t, c, newZid)
}
-func TestCreateGetRenameDeleteZettelJSON(t *testing.T) {
+func TestCreateGetRenameDeleteZettelData(t *testing.T) {
// Is not to be allowed to run in parallel with other tests.
c := getClient()
c.SetAuth("creator", "creator")
- zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{
+ zid, err := c.CreateZettelData(context.Background(), api.ZettelData{
Meta: nil,
Encoding: "",
Content: "Example",
})
if err != nil {
@@ -86,26 +89,26 @@
c.SetAuth("owner", "owner")
doDelete(t, c, newZid)
}
-func TestCreateGetDeleteZettelJSON(t *testing.T) {
+func TestCreateGetDeleteZettelData(t *testing.T) {
// Is not to be allowed to run in parallel with other tests.
c := getClient()
c.SetAuth("owner", "owner")
wrongModified := "19691231115959"
- zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{
+ zid, err := c.CreateZettelData(context.Background(), api.ZettelData{
Meta: api.ZettelMeta{
api.KeyTitle: "A\nTitle", // \n must be converted into a space
api.KeyModified: wrongModified,
},
})
if err != nil {
t.Error("Cannot create zettel:", err)
return
}
- z, err := c.GetZettelJSON(context.Background(), zid)
+ z, err := c.GetZettelData(context.Background(), zid)
if err != nil {
t.Error("Cannot get zettel:", zid, err)
} else {
exp := "A Title"
if got := z.Meta[api.KeyTitle]; got != exp {
@@ -150,14 +153,14 @@
}
// Must delete to clean up for next tests
doDelete(t, c, api.ZidDefaultHome)
}
-func TestUpdateZettelJSON(t *testing.T) {
+func TestUpdateZettelData(t *testing.T) {
c := getClient()
c.SetAuth("writer", "writer")
- z, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome)
+ z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
if err != nil {
t.Error(err)
return
}
if got := z.Meta[api.KeyTitle]; got != "Home" {
@@ -166,16 +169,16 @@
}
newTitle := "New Home"
z.Meta[api.KeyTitle] = newTitle
wrongModified := "19691231235959"
z.Meta[api.KeyModified] = wrongModified
- err = c.UpdateZettelJSON(context.Background(), api.ZidDefaultHome, z)
+ err = c.UpdateZettelData(context.Background(), api.ZidDefaultHome, z)
if err != nil {
t.Error(err)
return
}
- zt, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome)
+ zt, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
if err != nil {
t.Error(err)
return
}
if got := zt.Meta[api.KeyTitle]; got != newTitle {
Index: tests/client/embed_test.go
==================================================================
--- tests/client/embed_test.go
+++ tests/client/embed_test.go
@@ -1,23 +1,26 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 Detlef Stern
+// 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 client_test
import (
"context"
"strings"
"testing"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
)
const (
abcZid = api.ZettelID("20211020121000")
abc10Zid = api.ZettelID("20211020121100")
@@ -75,11 +78,11 @@
func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("reader", "reader")
- zettelData, err := c.GetZettelJSON(context.Background(), api.ZidEmoji)
+ zettelData, err := c.GetZettelData(context.Background(), api.ZidEmoji)
if err != nil {
t.Error(err)
return
}
expectedEnc := "base64"
@@ -90,11 +93,11 @@
content, err := c.GetEvaluatedZettel(context.Background(), abc10Zid, api.EncoderHTML)
if err != nil {
t.Error(err)
return
}
- if exp, got := "
", string(content); exp != got {
+ if exp, got := "", string(content); exp != got {
t.Errorf("Zettel %q must contain %q, but got %q", abc10Zid, exp, got)
}
}
func stringHead(s string) string {
Index: tests/markdown_test.go
==================================================================
--- tests/markdown_test.go
+++ tests/markdown_test.go
@@ -1,37 +1,43 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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 tests provides some higher-level tests.
package tests
import (
"bytes"
"encoding/json"
"fmt"
"os"
+ "strings"
"testing"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/input"
"zettelstore.de/z/ast"
+ "zettelstore.de/z/config"
"zettelstore.de/z/encoder"
_ "zettelstore.de/z/encoder/htmlenc"
- _ "zettelstore.de/z/encoder/sexprenc"
+ _ "zettelstore.de/z/encoder/mdenc"
+ _ "zettelstore.de/z/encoder/shtmlenc"
+ _ "zettelstore.de/z/encoder/szenc"
_ "zettelstore.de/z/encoder/textenc"
- _ "zettelstore.de/z/encoder/zjsonenc"
_ "zettelstore.de/z/encoder/zmkenc"
- "zettelstore.de/z/input"
"zettelstore.de/z/parser"
_ "zettelstore.de/z/parser/markdown"
_ "zettelstore.de/z/parser/zettelmark"
+ "zettelstore.de/z/zettel/meta"
)
type markdownTestCase struct {
Markdown string `json:"markdown"`
HTML string `json:"html"`
@@ -43,11 +49,11 @@
func TestEncoderAvailability(t *testing.T) {
t.Parallel()
encoderMissing := false
for _, enc := range encodings {
- enc := encoder.Create(enc)
+ enc := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN})
if enc == nil {
t.Errorf("No encoder for %q found", enc)
encoderMissing = true
}
}
@@ -66,52 +72,76 @@
if err = json.Unmarshal(content, &testcases); err != nil {
panic(err)
}
for _, tc := range testcases {
- ast := parser.ParseBlocks(input.NewInput([]byte(tc.Markdown)), nil, "markdown")
+ 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.ParseBlocks(input.NewInput([]byte(markdown)), nil, meta.SyntaxMarkdown, hi)
+}
func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
- var buf bytes.Buffer
+ var sb strings.Builder
testID := tc.Example*100 + 1
for _, enc := range encodings {
t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) {
- encoder.Create(enc).WriteBlocks(&buf, ast)
- buf.Reset()
+ encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}).WriteBlocks(&sb, ast)
+ sb.Reset()
})
}
}
func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
- zmkEncoder := encoder.Create(api.EncoderZmk)
+ zmkEncoder := encoder.Create(api.EncoderZmk, nil)
var buf bytes.Buffer
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.ParseBlocks(input.NewInput(buf.Bytes()), nil, api.ValueSyntaxZmk)
+ secondAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.SyntaxZmk, 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.ParseBlocks(input.NewInput(buf.Bytes()), nil, api.ValueSyntaxZmk)
+ thirdAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.SyntaxZmk, config.NoHTML)
buf.Reset()
zmkEncoder.WriteBlocks(&buf, &thirdAst)
gotThird := buf.String()
if gotSecond != gotThird {
st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird)
}
})
}
+
+func TestAdditionalMarkdown(t *testing.T) {
+ testcases := []struct {
+ md string
+ exp string
+ }{
+ {`abc
def`, `abc@@
@@{="html"}def`},
+ }
+ zmkEncoder := encoder.Create(api.EncoderZmk, nil)
+ var sb strings.Builder
+ for i, tc := range testcases {
+ ast := createMDBlockSlice(tc.md, config.MarkdownHTML)
+ sb.Reset()
+ zmkEncoder.WriteBlocks(&sb, &ast)
+ got := sb.String()
+ if got != tc.exp {
+ t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got)
+ }
+ }
+}
ADDED tests/naughtystrings_test.go
Index: tests/naughtystrings_test.go
==================================================================
--- tests/naughtystrings_test.go
+++ tests/naughtystrings_test.go
@@ -0,0 +1,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 tests
+
+import (
+ "bufio"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/input"
+ _ "zettelstore.de/z/cmd"
+ "zettelstore.de/z/encoder"
+ "zettelstore.de/z/parser"
+ "zettelstore.de/z/zettel/meta"
+)
+
+// 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")
+ file, err := os.Open(fpath)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ if text := scanner.Text(); text != "" && text[0] != '#' {
+ result = append(result, text)
+ }
+ }
+ return result, scanner.Err()
+}
+
+func getAllParser() (result []*parser.Info) {
+ for _, pname := range parser.GetSyntaxes() {
+ pinfo := parser.Get(pname)
+ if pname == pinfo.Name {
+ result = append(result, pinfo)
+ }
+ }
+ return result
+}
+
+func getAllEncoder() (result []encoder.Encoder) {
+ for _, enc := range encoder.GetEncodings() {
+ e := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN})
+ result = append(result, e)
+ }
+ return result
+}
+
+func TestNaughtyStringParser(t *testing.T) {
+ blns, err := getNaughtyStrings()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(blns) == 0 {
+ t.Fatal("no naughty strings found")
+ }
+ pinfos := getAllParser()
+ if len(pinfos) == 0 {
+ t.Fatal("no parser found")
+ }
+ encs := getAllEncoder()
+ if len(encs) == 0 {
+ t.Fatal("no encoder found")
+ }
+ for _, s := range blns {
+ for _, pinfo := range pinfos {
+ bs := pinfo.ParseBlocks(input.NewInput([]byte(s)), &meta.Meta{}, pinfo.Name)
+ is := pinfo.ParseInlines(input.NewInput([]byte(s)), pinfo.Name)
+ for _, enc := range encs {
+ _, err = enc.WriteBlocks(io.Discard, &bs)
+ if err != nil {
+ t.Error(err)
+ }
+ _, err = enc.WriteInlines(io.Discard, &is)
+ if err != nil {
+ t.Error(err)
+ }
+ }
+ }
+ }
+}
Index: tests/regression_test.go
==================================================================
--- tests/regression_test.go
+++ tests/regression_test.go
@@ -1,46 +1,49 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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 tests provides some higher-level tests.
package tests
import (
- "bytes"
"context"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
+ "strings"
"testing"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/ast"
"zettelstore.de/z/box"
"zettelstore.de/z/box/manager"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/config"
"zettelstore.de/z/encoder"
"zettelstore.de/z/kernel"
"zettelstore.de/z/parser"
+ "zettelstore.de/z/query"
+ "zettelstore.de/z/zettel/meta"
_ "zettelstore.de/z/box/dirbox"
)
var encodings = []api.EncodingEnum{
api.EncoderHTML,
- api.EncoderSexpr,
+ api.EncoderSz,
api.EncoderText,
- api.EncoderZJSON,
}
func getFileBoxes(wd, kind string) (root string, boxes []box.ManagedBox) {
root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind))
entries, err := os.ReadDir(root)
@@ -119,14 +122,14 @@
}
func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) {
t.Helper()
- if enc := encoder.Create(enc); enc != nil {
- var buf bytes.Buffer
- enc.WriteMeta(&buf, zn.Meta, parser.ParseMetadata)
- checkFileContent(t, resultName, buf.String())
+ if enc := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}); enc != nil {
+ var sf strings.Builder
+ enc.WriteMeta(&sf, zn.Meta, parser.ParseMetadata)
+ checkFileContent(t, resultName, sf.String())
return
}
panic(fmt.Sprintf("Unknown writer encoding %q", enc))
}
@@ -134,20 +137,21 @@
ss := p.(box.StartStopper)
if err := ss.Start(context.Background()); err != nil {
panic(err)
}
metaList := []*meta.Meta{}
- err := p.ApplyMeta(context.Background(), func(m *meta.Meta) { metaList = append(metaList, m) }, nil)
- if err != nil {
+ if err := p.ApplyMeta(context.Background(),
+ func(m *meta.Meta) { metaList = append(metaList, m) },
+ query.AlwaysIncluded); err != nil {
panic(err)
}
for _, meta := range metaList {
- zettel, err2 := p.GetZettel(context.Background(), meta.Zid)
- if err2 != nil {
- panic(err2)
+ zettel, err := p.GetZettel(context.Background(), meta.Zid)
+ if err != nil {
+ panic(err)
}
- z := parser.ParseZettel(zettel, "", testConfig)
+ z := parser.ParseZettel(context.Background(), zettel, "", testConfig)
for _, enc := range encodings {
t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, enc), func(st *testing.T) {
resultName := filepath.Join(wd, "result", "meta", boxName, z.Zid.String()+"."+enc.String())
checkMetaFile(st, resultName, z, enc)
})
@@ -156,16 +160,16 @@
ss.Stop(context.Background())
}
type myConfig struct{}
-func (*myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { return m }
-func (*myConfig) GetDefaultLang() string { return "" }
-func (*myConfig) GetFooterHTML() string { return "" }
-func (*myConfig) GetHomeZettel() id.Zid { return id.Invalid }
+func (*myConfig) Get(context.Context, *meta.Meta, string) string { return "" }
+func (*myConfig) AddDefaultValues(_ context.Context, m *meta.Meta) *meta.Meta {
+ return m
+}
+func (*myConfig) GetHTMLInsecurity() config.HTMLInsecurity { return config.NoHTML }
func (*myConfig) GetListPageSize() int { return 0 }
-func (*myConfig) GetMarkerExternal() string { return "" }
func (*myConfig) GetSiteName() string { return "" }
func (*myConfig) GetYAMLHeader() bool { return false }
func (*myConfig) GetZettelFileSyntax() []string { return nil }
func (*myConfig) GetSimpleMode() bool { return false }
DELETED tools/build.go
Index: tools/build.go
==================================================================
--- tools/build.go
+++ tools/build.go
@@ -1,560 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 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.
-//-----------------------------------------------------------------------------
-
-// Package main provides a command to build and run the software.
-package main
-
-import (
- "archive/zip"
- "bytes"
- "errors"
- "flag"
- "fmt"
- "io"
- "io/fs"
- "net"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "time"
-
- "zettelstore.de/z/strfun"
-)
-
-var directProxy = []string{"GOPROXY=direct"}
-
-func executeCommand(env []string, name string, arg ...string) (string, error) {
- logCommand("EXEC", env, name, arg)
- var out bytes.Buffer
- cmd := prepareCommand(env, name, arg, &out)
- err := cmd.Run()
- return out.String(), err
-}
-
-func prepareCommand(env []string, name string, arg []string, out io.Writer) *exec.Cmd {
- if len(env) > 0 {
- env = append(env, os.Environ()...)
- }
- cmd := exec.Command(name, arg...)
- cmd.Env = env
- cmd.Stdin = nil
- cmd.Stdout = out
- cmd.Stderr = os.Stderr
- return cmd
-}
-
-func logCommand(exec string, env []string, name string, arg []string) {
- if verbose {
- if len(env) > 0 {
- for i, e := range env {
- fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e)
- }
- }
- fmt.Fprintln(os.Stderr, exec, name, arg)
- }
-}
-
-func readVersionFile() (string, error) {
- content, err := os.ReadFile("VERSION")
- if err != nil {
- return "", err
- }
- return strings.TrimFunc(string(content), func(r rune) bool {
- return r <= ' '
- }), nil
-}
-
-func getVersion() string {
- base, err := readVersionFile()
- if err != nil {
- base = "dev"
- }
- return base
-}
-
-var dirtyPrefixes = []string{
- "DELETED ", "ADDED ", "UPDATED ", "CONFLICT ", "EDITED ", "RENAMED ", "EXTRA "}
-
-const dirtySuffix = "-dirty"
-
-func readFossilDirty() (string, error) {
- s, err := executeCommand(nil, "fossil", "status", "--differ")
- if err != nil {
- return "", err
- }
- for _, line := range strfun.SplitLines(s) {
- for _, prefix := range dirtyPrefixes {
- if strings.HasPrefix(line, prefix) {
- return dirtySuffix, nil
- }
- }
- }
- return "", nil
-}
-
-func getFossilDirty() string {
- fossil, err := readFossilDirty()
- if err != nil {
- return ""
- }
- return fossil
-}
-
-func findExec(cmd string) string {
- if path, err := executeCommand(nil, "which", cmd); err == nil && path != "" {
- return strings.TrimSpace(path)
- }
- return ""
-}
-
-func cmdCheck(forRelease bool) error {
- if err := checkGoTest("./..."); err != nil {
- return err
- }
- if err := checkGoVet(); err != nil {
- return err
- }
- if err := checkShadow(forRelease); err != nil {
- return err
- }
- if err := checkStaticcheck(); err != nil {
- return err
- }
- if err := checkUnparam(forRelease); err != nil {
- return err
- }
- return checkFossilExtra()
-}
-
-func checkGoTest(pkg string, testParams ...string) error {
- args := []string{"test", pkg}
- args = append(args, testParams...)
- out, err := executeCommand(directProxy, "go", args...)
- if err != nil {
- for _, line := range strfun.SplitLines(out) {
- if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") {
- continue
- }
- fmt.Fprintln(os.Stderr, line)
- }
- }
- return err
-}
-
-func checkGoVet() error {
- out, err := executeCommand(nil, "go", "vet", "./...")
- if err != nil {
- fmt.Fprintln(os.Stderr, "Some checks failed")
- if len(out) > 0 {
- fmt.Fprintln(os.Stderr, out)
- }
- }
- return err
-}
-
-func checkShadow(forRelease bool) error {
- path, err := findExecStrict("shadow", forRelease)
- if path == "" {
- return err
- }
- out, err := executeCommand(nil, path, "-strict", "./...")
- if err != nil {
- fmt.Fprintln(os.Stderr, "Some shadowed variables found")
- if len(out) > 0 {
- fmt.Fprintln(os.Stderr, out)
- }
- }
- return err
-}
-
-func checkStaticcheck() error {
- out, err := executeCommand(nil, "staticcheck", "./...")
- if err != nil {
- fmt.Fprintln(os.Stderr, "Some staticcheck problems found")
- if len(out) > 0 {
- fmt.Fprintln(os.Stderr, out)
- }
- }
- return err
-}
-
-func checkUnparam(forRelease bool) error {
- path, err := findExecStrict("unparam", forRelease)
- if path == "" {
- return err
- }
- // out, err := executeCommand(nil, path, "./...")
- // if err != nil {
- // fmt.Fprintln(os.Stderr, "Some unparam problems found")
- // if len(out) > 0 {
- // fmt.Fprintln(os.Stderr, out)
- // }
- // }
- // if forRelease {
- // if out2, err2 := executeCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil {
- // fmt.Fprintln(os.Stderr, "Some optional unparam problems found")
- // if len(out2) > 0 {
- // fmt.Fprintln(os.Stderr, out2)
- // }
- // }
- // }
- return err
-}
-
-func findExecStrict(cmd string, forRelease bool) (string, error) {
- path := findExec(cmd)
- if path != "" || !forRelease {
- return path, nil
- }
- return "", errors.New("Command '" + cmd + "' not installed, but required for release")
-}
-
-func checkFossilExtra() error {
- out, err := executeCommand(nil, "fossil", "extra")
- if err != nil {
- fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'")
- return err
- }
- if len(out) > 0 {
- fmt.Fprint(os.Stderr, "Warning: unversioned file(s):")
- for i, extra := range strfun.SplitLines(out) {
- if i > 0 {
- fmt.Fprint(os.Stderr, ",")
- }
- fmt.Fprintf(os.Stderr, " %q", extra)
- }
- fmt.Fprintln(os.Stderr)
- }
- return nil
-}
-
-type zsInfo struct {
- cmd *exec.Cmd
- out bytes.Buffer
- adminAddress string
-}
-
-func cmdTestAPI() error {
- var err error
- var info zsInfo
- needServer := !addressInUse(":23123")
- if needServer {
- err = startZettelstore(&info)
- }
- if err != nil {
- return err
- }
- err = checkGoTest("zettelstore.de/z/tests/client", "-base-url", "http://127.0.0.1:23123")
- if needServer {
- err1 := stopZettelstore(&info)
- if err == nil {
- err = err1
- }
- }
- return err
-}
-
-func startZettelstore(info *zsInfo) error {
- info.adminAddress = ":2323"
- name, arg := "go", []string{
- "run", "cmd/zettelstore/main.go", "run",
- "-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]}
- logCommand("FORK", nil, name, arg)
- cmd := prepareCommand(nil, name, arg, &info.out)
- if !verbose {
- cmd.Stderr = nil
- }
- err := cmd.Start()
- for i := 0; i < 100; i++ {
- time.Sleep(time.Millisecond * 100)
- if addressInUse(info.adminAddress) {
- info.cmd = cmd
- return err
- }
- }
- return errors.New("zettelstore did not start")
-}
-
-func stopZettelstore(i *zsInfo) error {
- conn, err := net.Dial("tcp", i.adminAddress)
- if err != nil {
- fmt.Println("Unable to stop Zettelstore")
- return err
- }
- io.WriteString(conn, "shutdown\n")
- conn.Close()
- err = i.cmd.Wait()
- return err
-}
-
-func addressInUse(address string) bool {
- conn, err := net.Dial("tcp", address)
- if err != nil {
- return false
- }
- conn.Close()
- return true
-}
-
-func cmdBuild() error {
- return doBuild(directProxy, getVersion(), "bin/zettelstore")
-}
-
-func doBuild(env []string, version, target string) error {
- out, err := executeCommand(
- env,
- "go", "build",
- "-tags", "osusergo,netgo",
- "-trimpath",
- "-ldflags", fmt.Sprintf("-X main.version=%v -w", version),
- "-o", target,
- "zettelstore.de/z/cmd/zettelstore",
- )
- if err != nil {
- return err
- }
- if len(out) > 0 {
- fmt.Println(out)
- }
- return nil
-}
-
-func cmdManual() error {
- base := getReleaseVersionData()
- return createManualZip(".", base)
-}
-
-func createManualZip(path, base string) error {
- manualPath := filepath.Join("docs", "manual")
- entries, err := os.ReadDir(manualPath)
- if err != nil {
- return err
- }
- zipName := filepath.Join(path, "manual-"+base+".zip")
- zipFile, err := os.OpenFile(zipName, os.O_RDWR|os.O_CREATE, 0600)
- if err != nil {
- return err
- }
- defer zipFile.Close()
- zipWriter := zip.NewWriter(zipFile)
- defer zipWriter.Close()
-
- for _, entry := range entries {
- if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil {
- return err
- }
- }
- return nil
-}
-
-func createManualZipEntry(path string, entry fs.DirEntry, zipWriter *zip.Writer) error {
- info, err := entry.Info()
- if err != nil {
- return err
- }
- fh, err := zip.FileInfoHeader(info)
- if err != nil {
- return err
- }
- fh.Name = entry.Name()
- fh.Method = zip.Deflate
- w, err := zipWriter.CreateHeader(fh)
- if err != nil {
- return err
- }
- manualFile, err := os.Open(filepath.Join(path, entry.Name()))
- if err != nil {
- return err
- }
- defer manualFile.Close()
- _, err = io.Copy(w, manualFile)
- return err
-}
-
-func getReleaseVersionData() string {
- if fossil := getFossilDirty(); fossil != "" {
- fmt.Fprintln(os.Stderr, "Warning: releasing a dirty version")
- }
- base := getVersion()
- if strings.HasSuffix(base, "dev") {
- return base[:len(base)-3] + "preview-" + time.Now().Format("20060102")
- }
- return base
-}
-
-func cmdRelease() error {
- if err := cmdCheck(true); err != nil {
- return err
- }
- base := getReleaseVersionData()
- releases := []struct {
- arch string
- os string
- env []string
- name string
- }{
- {"amd64", "linux", nil, "zettelstore"},
- {"arm", "linux", []string{"GOARM=6"}, "zettelstore"},
- {"amd64", "darwin", nil, "zettelstore"},
- {"arm64", "darwin", nil, "zettelstore"},
- {"amd64", "windows", nil, "zettelstore.exe"},
- }
- for _, rel := range releases {
- env := append([]string{}, rel.env...)
- env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os)
- env = append(env, directProxy...)
- zsName := filepath.Join("releases", rel.name)
- if err := doBuild(env, base, zsName); err != nil {
- return err
- }
- zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch)
- if err := createReleaseZip(zsName, zipName, rel.name); err != nil {
- return err
- }
- if err := os.Remove(zsName); err != nil {
- return err
- }
- }
- return createManualZip("releases", base)
-}
-
-func createReleaseZip(zsName, zipName, fileName string) error {
- zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600)
- if err != nil {
- return err
- }
- defer zipFile.Close()
- zw := zip.NewWriter(zipFile)
- defer zw.Close()
- err = addFileToZip(zw, zsName, fileName)
- if err != nil {
- return err
- }
- err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt")
- if err != nil {
- return err
- }
- err = addFileToZip(zw, "docs/readmezip.txt", "README.txt")
- return err
-}
-
-func addFileToZip(zipFile *zip.Writer, filepath, filename string) error {
- zsFile, err := os.Open(filepath)
- if err != nil {
- return err
- }
- defer zsFile.Close()
- stat, err := zsFile.Stat()
- if err != nil {
- return err
- }
- fh, err := zip.FileInfoHeader(stat)
- if err != nil {
- return err
- }
- fh.Name = filename
- fh.Method = zip.Deflate
- w, err := zipFile.CreateHeader(fh)
- if err != nil {
- return err
- }
- _, err = io.Copy(w, zsFile)
- return err
-}
-
-func cmdClean() error {
- for _, dir := range []string{"bin", "releases"} {
- err := os.RemoveAll(dir)
- if err != nil {
- return err
- }
- }
- out, err := executeCommand(nil, "go", "clean", "./...")
- if err != nil {
- return err
- }
- if len(out) > 0 {
- fmt.Println(out)
- }
- out, err = executeCommand(nil, "go", "clean", "-cache", "-modcache", "-testcache")
- if err != nil {
- return err
- }
- if len(out) > 0 {
- fmt.Println(out)
- }
- return nil
-}
-
-func cmdHelp() {
- fmt.Println(`Usage: go run tools/build.go [-v] COMMAND
-
-Options:
- -v Verbose output.
-
-Commands:
- build Build the software for local computer.
- check Check current working state: execute tests,
- static analysis tools, extra files, ...
- Is automatically done when releasing the software.
- clean Remove all build and release directories.
- help Output this text.
- manual Create a ZIP file with all manual zettel
- relcheck Check current working state for release.
- release Create the software for various platforms and put them in
- appropriate named ZIP files.
- testapi Start a Zettelstore and execute API tests.
- version Print the current version of the software.
-
-All commands can be abbreviated as long as they remain unique.`)
-}
-
-var (
- verbose bool
-)
-
-func main() {
- flag.BoolVar(&verbose, "v", false, "Verbose output")
- flag.Parse()
- var err error
- args := flag.Args()
- if len(args) < 1 {
- cmdHelp()
- } else {
- switch args[0] {
- case "b", "bu", "bui", "buil", "build":
- err = cmdBuild()
- case "m", "ma", "man", "manu", "manua", "manual":
- err = cmdManual()
- case "r", "re", "rel", "rele", "relea", "releas", "release":
- err = cmdRelease()
- case "cl", "cle", "clea", "clean":
- err = cmdClean()
- case "v", "ve", "ver", "vers", "versi", "versio", "version":
- fmt.Print(getVersion())
- case "ch", "che", "chec", "check":
- err = cmdCheck(false)
- case "relc", "relch", "relche", "relchec", "relcheck":
- err = cmdCheck(true)
- case "t", "te", "tes", "test", "testa", "testap", "testapi":
- cmdTestAPI()
- case "h", "he", "hel", "help":
- cmdHelp()
- default:
- fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0])
- cmdHelp()
- os.Exit(1)
- }
- }
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- }
-}
ADDED tools/build/build.go
Index: tools/build/build.go
==================================================================
--- tools/build/build.go
+++ tools/build/build.go
@@ -0,0 +1,329 @@
+//-----------------------------------------------------------------------------
+// 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 main provides a command to build and run the software.
+package main
+
+import (
+ "archive/zip"
+ "bytes"
+ "flag"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/input"
+ "zettelstore.de/z/strfun"
+ "zettelstore.de/z/tools"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
+
+func readVersionFile() (string, error) {
+ content, err := os.ReadFile("VERSION")
+ if err != nil {
+ return "", err
+ }
+ return strings.TrimFunc(string(content), func(r rune) bool {
+ return r <= ' '
+ }), nil
+}
+
+func getVersion() string {
+ base, err := readVersionFile()
+ if err != nil {
+ base = "dev"
+ }
+ return base
+}
+
+var dirtyPrefixes = []string{
+ "DELETED ", "ADDED ", "UPDATED ", "CONFLICT ", "EDITED ", "RENAMED ", "EXTRA "}
+
+const dirtySuffix = "-dirty"
+
+func readFossilDirty() (string, error) {
+ s, err := tools.ExecuteCommand(nil, "fossil", "status", "--differ")
+ if err != nil {
+ return "", err
+ }
+ for _, line := range strfun.SplitLines(s) {
+ for _, prefix := range dirtyPrefixes {
+ if strings.HasPrefix(line, prefix) {
+ return dirtySuffix, nil
+ }
+ }
+ }
+ return "", nil
+}
+
+func getFossilDirty() string {
+ fossil, err := readFossilDirty()
+ if err != nil {
+ return ""
+ }
+ return fossil
+}
+
+func cmdBuild() error {
+ return doBuild(tools.EnvDirectProxy, getVersion(), "bin/zettelstore")
+}
+
+func doBuild(env []string, version, target string) error {
+ env = append(env, "CGO_ENABLED=0")
+ env = append(env, tools.EnvGoVCS...)
+ out, err := tools.ExecuteCommand(
+ env,
+ "go", "build",
+ "-tags", "osusergo,netgo",
+ "-trimpath",
+ "-ldflags", fmt.Sprintf("-X main.version=%v -w", version),
+ "-o", target,
+ "zettelstore.de/z/cmd/zettelstore",
+ )
+ if err != nil {
+ return err
+ }
+ if len(out) > 0 {
+ fmt.Println(out)
+ }
+ return nil
+}
+
+func cmdHelp() {
+ fmt.Println(`Usage: go run tools/build/build.go [-v] COMMAND
+
+Options:
+ -v Verbose output.
+
+Commands:
+ build Build the software for local computer.
+ help Output this text.
+ manual Create a ZIP file with all manual zettel
+ release Create the software for various platforms and put them in
+ appropriate named ZIP files.
+ version Print the current version of the software.
+
+All commands can be abbreviated as long as they remain unique.`)
+}
+
+func main() {
+ flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
+ flag.Parse()
+ var err error
+ args := flag.Args()
+ if len(args) < 1 {
+ cmdHelp()
+ } else {
+ switch args[0] {
+ case "b", "bu", "bui", "buil", "build":
+ err = cmdBuild()
+ case "m", "ma", "man", "manu", "manua", "manual":
+ err = cmdManual()
+ case "r", "re", "rel", "rele", "relea", "releas", "release":
+ err = cmdRelease()
+ case "v", "ve", "ver", "vers", "versi", "versio", "version":
+ fmt.Print(getVersion())
+ case "h", "he", "hel", "help":
+ cmdHelp()
+ default:
+ fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0])
+ cmdHelp()
+ os.Exit(1)
+ }
+ }
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+}
+
+// --- manual
+
+func cmdManual() error {
+ base := getReleaseVersionData()
+ return createManualZip(".", base)
+}
+
+func createManualZip(path, base string) error {
+ manualPath := filepath.Join("docs", "manual")
+ entries, err := os.ReadDir(manualPath)
+ if err != nil {
+ return err
+ }
+ zipName := filepath.Join(path, "manual-"+base+".zip")
+ zipFile, err := os.OpenFile(zipName, os.O_RDWR|os.O_CREATE, 0600)
+ if err != nil {
+ return err
+ }
+ defer zipFile.Close()
+ zipWriter := zip.NewWriter(zipFile)
+ defer zipWriter.Close()
+
+ for _, entry := range entries {
+ if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+const versionZid = "00001000000001"
+
+func createManualZipEntry(path string, entry fs.DirEntry, zipWriter *zip.Writer) error {
+ info, err := entry.Info()
+ if err != nil {
+ return err
+ }
+ fh, err := zip.FileInfoHeader(info)
+ if err != nil {
+ return err
+ }
+ name := entry.Name()
+ fh.Name = name
+ fh.Method = zip.Deflate
+ w, err := zipWriter.CreateHeader(fh)
+ if err != nil {
+ return err
+ }
+ manualFile, err := os.Open(filepath.Join(path, name))
+ if err != nil {
+ return err
+ }
+ defer manualFile.Close()
+
+ if name != versionZid+".zettel" {
+ _, err = io.Copy(w, manualFile)
+ return err
+ }
+
+ data, err := io.ReadAll(manualFile)
+ if err != nil {
+ return err
+ }
+ inp := input.NewInput(data)
+ m := meta.NewFromInput(id.MustParse(versionZid), inp)
+ m.SetNow(api.KeyModified)
+
+ var buf bytes.Buffer
+ if _, err = fmt.Fprintf(&buf, "id: %s\n", versionZid); err != nil {
+ return err
+ }
+ if _, err = m.WriteComputed(&buf); err != nil {
+ return err
+ }
+ version := getVersion()
+ if _, err = fmt.Fprintf(&buf, "\n%s", version); err != nil {
+ return err
+ }
+ _, err = io.Copy(w, &buf)
+ return err
+}
+
+//--- release
+
+func cmdRelease() error {
+ if err := tools.Check(true); err != nil {
+ return err
+ }
+ base := getReleaseVersionData()
+ releases := []struct {
+ arch string
+ os string
+ env []string
+ name string
+ }{
+ {"amd64", "linux", nil, "zettelstore"},
+ {"arm", "linux", []string{"GOARM=6"}, "zettelstore"},
+ {"amd64", "darwin", nil, "zettelstore"},
+ {"arm64", "darwin", nil, "zettelstore"},
+ {"amd64", "windows", nil, "zettelstore.exe"},
+ }
+ for _, rel := range releases {
+ env := append([]string{}, rel.env...)
+ env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os)
+ env = append(env, tools.EnvDirectProxy...)
+ env = append(env, tools.EnvGoVCS...)
+ zsName := filepath.Join("releases", rel.name)
+ if err := doBuild(env, base, zsName); err != nil {
+ return err
+ }
+ zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch)
+ if err := createReleaseZip(zsName, zipName, rel.name); err != nil {
+ return err
+ }
+ if err := os.Remove(zsName); err != nil {
+ return err
+ }
+ }
+ return createManualZip("releases", base)
+}
+
+func getReleaseVersionData() string {
+ if fossil := getFossilDirty(); fossil != "" {
+ fmt.Fprintln(os.Stderr, "Warning: releasing a dirty version")
+ }
+ base := getVersion()
+ if strings.HasSuffix(base, "dev") {
+ return base[:len(base)-3] + "preview-" + time.Now().Local().Format("20060102")
+ }
+ return base
+}
+
+func createReleaseZip(zsName, zipName, fileName string) error {
+ zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600)
+ if err != nil {
+ return err
+ }
+ defer zipFile.Close()
+ zw := zip.NewWriter(zipFile)
+ defer zw.Close()
+ err = addFileToZip(zw, zsName, fileName)
+ if err != nil {
+ return err
+ }
+ err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt")
+ if err != nil {
+ return err
+ }
+ err = addFileToZip(zw, "docs/readmezip.txt", "README.txt")
+ return err
+}
+
+func addFileToZip(zipFile *zip.Writer, filepath, filename string) error {
+ zsFile, err := os.Open(filepath)
+ if err != nil {
+ return err
+ }
+ defer zsFile.Close()
+ stat, err := zsFile.Stat()
+ if err != nil {
+ return err
+ }
+ fh, err := zip.FileInfoHeader(stat)
+ if err != nil {
+ return err
+ }
+ fh.Name = filename
+ fh.Method = zip.Deflate
+ w, err := zipFile.CreateHeader(fh)
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(w, zsFile)
+ return err
+}
ADDED tools/check/check.go
Index: tools/check/check.go
==================================================================
--- tools/check/check.go
+++ tools/check/check.go
@@ -0,0 +1,35 @@
+//-----------------------------------------------------------------------------
+// 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 main provides a command to execute unit tests.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+
+ "zettelstore.de/z/tools"
+)
+
+var release bool
+
+func main() {
+ flag.BoolVar(&release, "r", false, "Release check")
+ flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
+ flag.Parse()
+
+ if err := tools.Check(release); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+}
ADDED tools/clean/clean.go
Index: tools/clean/clean.go
==================================================================
--- tools/clean/clean.go
+++ tools/clean/clean.go
@@ -0,0 +1,56 @@
+//-----------------------------------------------------------------------------
+// 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 main provides a command to clean / remove development artifacts.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+
+ "zettelstore.de/z/tools"
+)
+
+func main() {
+ flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
+ flag.Parse()
+
+ if err := cmdClean(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+}
+
+func cmdClean() error {
+ for _, dir := range []string{"bin", "releases"} {
+ err := os.RemoveAll(dir)
+ if err != nil {
+ return err
+ }
+ }
+ out, err := tools.ExecuteCommand(nil, "go", "clean", "./...")
+ if err != nil {
+ return err
+ }
+ if len(out) > 0 {
+ fmt.Println(out)
+ }
+ out, err = tools.ExecuteCommand(nil, "go", "clean", "-cache", "-modcache", "-testcache")
+ if err != nil {
+ return err
+ }
+ if len(out) > 0 {
+ fmt.Println(out)
+ }
+ return nil
+}
ADDED tools/devtools/devtools.go
Index: tools/devtools/devtools.go
==================================================================
--- tools/devtools/devtools.go
+++ tools/devtools/devtools.go
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------------
+// 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 main provides a command to install development tools.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+
+ "zettelstore.de/z/tools"
+)
+
+func main() {
+ flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
+ flag.Parse()
+
+ if err := cmdTools(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+}
+
+func cmdTools() error {
+ tools := []struct{ name, pack string }{
+ {"shadow", "golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest"},
+ {"unparam", "mvdan.cc/unparam@latest"},
+ {"staticcheck", "honnef.co/go/tools/cmd/staticcheck@latest"},
+ {"govulncheck", "golang.org/x/vuln/cmd/govulncheck@latest"},
+ {"deadcode", "golang.org/x/tools/cmd/deadcode@latest"},
+ {"errcheck", "github.com/kisielk/errcheck@latest"},
+ }
+ for _, tool := range tools {
+ err := doGoInstall(tool.pack)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+func doGoInstall(pack string) error {
+ out, err := tools.ExecuteCommand(nil, "go", "install", pack)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Unable to install package", pack)
+ if len(out) > 0 {
+ fmt.Fprintln(os.Stderr, out)
+ }
+ }
+ return err
+}
ADDED tools/htmllint/htmllint.go
Index: tools/htmllint/htmllint.go
==================================================================
--- tools/htmllint/htmllint.go
+++ tools/htmllint/htmllint.go
@@ -0,0 +1,205 @@
+//-----------------------------------------------------------------------------
+// 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 main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log"
+ "math/rand/v2"
+ "net/url"
+ "os"
+ "regexp"
+ "sort"
+ "strings"
+
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/client"
+ "zettelstore.de/z/tools"
+)
+
+func main() {
+ flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
+ flag.Parse()
+
+ if err := cmdValidateHTML(flag.Args()); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+}
+func cmdValidateHTML(args []string) error {
+ rawURL := "http://localhost:23123"
+ if len(args) > 0 {
+ rawURL = args[0]
+ }
+ u, err := url.Parse(rawURL)
+ if err != nil {
+ return err
+ }
+ client := client.NewClient(u)
+ _, _, metaList, err := client.QueryZettelData(context.Background(), "")
+ if err != nil {
+ return err
+ }
+ zids, perm := calculateZids(metaList)
+ for _, kd := range keyDescr {
+ msgCount := 0
+ fmt.Fprintf(os.Stderr, "Now checking: %s\n", kd.text)
+ for _, zid := range zidsToUse(zids, perm, kd.sampleSize) {
+ var nmsgs int
+ nmsgs, err = validateHTML(client, kd.uc, api.ZettelID(zid))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "* error while validating zettel %v with: %v\n", zid, err)
+ msgCount += 1
+ } else {
+ msgCount += nmsgs
+ }
+ }
+ if msgCount == 1 {
+ fmt.Fprintln(os.Stderr, "==> found 1 possible issue")
+ } else if msgCount > 1 {
+ fmt.Fprintf(os.Stderr, "==> found %v possible issues\n", msgCount)
+ }
+ }
+ return nil
+}
+
+func calculateZids(metaList []api.ZidMetaRights) ([]string, []int) {
+ zids := make([]string, len(metaList))
+ for i, m := range metaList {
+ zids[i] = string(m.ID)
+ }
+ sort.Strings(zids)
+ return zids, rand.Perm(len(metaList))
+}
+
+func zidsToUse(zids []string, perm []int, sampleSize int) []string {
+ if sampleSize < 0 || len(perm) <= sampleSize {
+ return zids
+ }
+ if sampleSize == 0 {
+ return nil
+ }
+ result := make([]string, sampleSize)
+ for i := range sampleSize {
+ result[i] = zids[perm[i]]
+ }
+ sort.Strings(result)
+ return result
+}
+
+var keyDescr = []struct {
+ uc urlCreator
+ text string
+ sampleSize int
+}{
+ {getHTMLZettel, "zettel HTML encoding", -1},
+ {createJustKey('h'), "zettel web view", -1},
+ {createJustKey('i'), "zettel info view", -1},
+ {createJustKey('e'), "zettel edit form", 100},
+ {createJustKey('c'), "zettel create form", 10},
+ {createJustKey('b'), "zettel rename form", 100},
+ {createJustKey('d'), "zettel delete dialog", 200},
+}
+
+type urlCreator func(*client.Client, api.ZettelID) *api.URLBuilder
+
+func createJustKey(key byte) urlCreator {
+ return func(c *client.Client, zid api.ZettelID) *api.URLBuilder {
+ return c.NewURLBuilder(key).SetZid(zid)
+ }
+}
+
+func getHTMLZettel(client *client.Client, zid api.ZettelID) *api.URLBuilder {
+ return client.NewURLBuilder('z').SetZid(zid).
+ AppendKVQuery(api.QueryKeyEncoding, api.EncodingHTML).
+ AppendKVQuery(api.QueryKeyPart, api.PartZettel)
+}
+
+func validateHTML(client *client.Client, uc urlCreator, zid api.ZettelID) (int, error) {
+ ub := uc(client, zid)
+ if tools.Verbose {
+ fmt.Fprintf(os.Stderr, "GET %v\n", ub)
+ }
+ data, err := client.Get(context.Background(), ub)
+ if err != nil {
+ return 0, err
+ }
+ if len(data) == 0 {
+ return 0, nil
+ }
+ _, stderr, err := tools.ExecuteFilter(data, nil, "tidy", "-e", "-q", "-lang", "en")
+ if err != nil {
+ switch err.Error() {
+ case "exit status 1":
+ case "exit status 2":
+ default:
+ log.Println("SERR", stderr)
+ return 0, err
+ }
+ }
+ if stderr == "" {
+ return 0, nil
+ }
+ if msgs := filterTidyMessages(strings.Split(stderr, "\n")); len(msgs) > 0 {
+ fmt.Fprintln(os.Stderr, zid)
+ for _, msg := range msgs {
+ fmt.Fprintln(os.Stderr, "-", msg)
+ }
+ return len(msgs), nil
+ }
+ return 0, nil
+}
+
+var reLine = regexp.MustCompile(`line \d+ column \d+ - (.+): (.+)`)
+
+func filterTidyMessages(lines []string) []string {
+ result := make([]string, 0, len(lines))
+ for _, line := range lines {
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ matches := reLine.FindStringSubmatch(line)
+ if len(matches) <= 1 {
+ if line == "This document has errors that must be fixed before" ||
+ line == "using HTML Tidy to generate a tidied up version." {
+ continue
+ }
+ result = append(result, "!!!"+line)
+ continue
+ }
+ if matches[1] == "Error" {
+ if len(matches) > 2 {
+ if matches[2] == "
is not recognized!" {
+ continue
+ }
+ }
+ }
+ if matches[1] != "Warning" {
+ result = append(result, "???"+line)
+ continue
+ }
+ if len(matches) > 2 {
+ switch matches[2] {
+ case "discarding unexpected ",
+ "discarding unexpected ",
+ ` proprietary attribute "inputmode"`:
+ continue
+ }
+ }
+ result = append(result, line)
+ }
+ return result
+}
ADDED tools/testapi/testapi.go
Index: tools/testapi/testapi.go
==================================================================
--- tools/testapi/testapi.go
+++ tools/testapi/testapi.go
@@ -0,0 +1,108 @@
+//-----------------------------------------------------------------------------
+// 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 main provides a command to test the API
+package main
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "net"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ "zettelstore.de/z/tools"
+)
+
+func main() {
+ flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
+ flag.Parse()
+
+ if err := cmdTestAPI(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+}
+
+type zsInfo struct {
+ cmd *exec.Cmd
+ out strings.Builder
+ adminAddress string
+}
+
+func cmdTestAPI() error {
+ var err error
+ var info zsInfo
+ needServer := !addressInUse(":23123")
+ if needServer {
+ err = startZettelstore(&info)
+ }
+ if err != nil {
+ return err
+ }
+ err = tools.CheckGoTest("zettelstore.de/z/tests/client", "-base-url", "http://127.0.0.1:23123")
+ if needServer {
+ err1 := stopZettelstore(&info)
+ if err == nil {
+ err = err1
+ }
+ }
+ return err
+}
+
+func startZettelstore(info *zsInfo) error {
+ info.adminAddress = ":2323"
+ name, arg := "go", []string{
+ "run", "cmd/zettelstore/main.go", "run",
+ "-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]}
+ tools.LogCommand("FORK", nil, name, arg)
+ cmd := tools.PrepareCommand(tools.EnvGoVCS, name, arg, nil, &info.out, os.Stderr)
+ if !tools.Verbose {
+ cmd.Stderr = nil
+ }
+ err := cmd.Start()
+ time.Sleep(2 * time.Second)
+ for range 100 {
+ time.Sleep(time.Millisecond * 100)
+ if addressInUse(info.adminAddress) {
+ info.cmd = cmd
+ return err
+ }
+ }
+ time.Sleep(4 * time.Second) // Wait for all zettel to be indexed.
+ return errors.New("zettelstore did not start")
+}
+
+func stopZettelstore(i *zsInfo) error {
+ conn, err := net.Dial("tcp", i.adminAddress)
+ if err != nil {
+ fmt.Println("Unable to stop Zettelstore")
+ return err
+ }
+ io.WriteString(conn, "shutdown\n")
+ conn.Close()
+ err = i.cmd.Wait()
+ return err
+}
+
+func addressInUse(address string) bool {
+ conn, err := net.Dial("tcp", address)
+ if err != nil {
+ return false
+ }
+ conn.Close()
+ return true
+}
ADDED tools/tools.go
Index: tools/tools.go
==================================================================
--- tools/tools.go
+++ tools/tools.go
@@ -0,0 +1,214 @@
+//-----------------------------------------------------------------------------
+// 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 tools provides a collection of functions to build needed tools.
+package tools
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "strings"
+
+ "zettelstore.de/z/strfun"
+)
+
+var EnvDirectProxy = []string{"GOPROXY=direct"}
+var EnvGoVCS = []string{"GOVCS=zettelstore.de:fossil,t73f.de:fossil"}
+var Verbose bool
+
+func ExecuteCommand(env []string, name string, arg ...string) (string, error) {
+ LogCommand("EXEC", env, name, arg)
+ var out strings.Builder
+ cmd := PrepareCommand(env, name, arg, nil, &out, os.Stderr)
+ err := cmd.Run()
+ return out.String(), err
+}
+
+func ExecuteFilter(data []byte, env []string, name string, arg ...string) (string, string, error) {
+ LogCommand("EXEC", env, name, arg)
+ var stdout, stderr strings.Builder
+ cmd := PrepareCommand(env, name, arg, bytes.NewReader(data), &stdout, &stderr)
+ err := cmd.Run()
+ return stdout.String(), stderr.String(), err
+}
+
+func PrepareCommand(env []string, name string, arg []string, in io.Reader, stdout, stderr io.Writer) *exec.Cmd {
+ if len(env) > 0 {
+ env = append(env, os.Environ()...)
+ }
+ cmd := exec.Command(name, arg...)
+ cmd.Env = env
+ cmd.Stdin = in
+ cmd.Stdout = stdout
+ cmd.Stderr = stderr
+ return cmd
+}
+func LogCommand(exec string, env []string, name string, arg []string) {
+ if Verbose {
+ if len(env) > 0 {
+ for i, e := range env {
+ fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e)
+ }
+ }
+ fmt.Fprintln(os.Stderr, exec, name, arg)
+ }
+}
+
+func Check(forRelease bool) error {
+ if err := CheckGoTest("./..."); err != nil {
+ return err
+ }
+ if err := checkGoVet(); err != nil {
+ return err
+ }
+ if err := checkShadow(forRelease); err != nil {
+ return err
+ }
+ if err := checkStaticcheck(); err != nil {
+ return err
+ }
+ if err := checkUnparam(forRelease); err != nil {
+ return err
+ }
+ if forRelease {
+ if err := checkGoVulncheck(); err != nil {
+ return err
+ }
+ }
+ return checkFossilExtra()
+}
+
+func CheckGoTest(pkg string, testParams ...string) error {
+ var env []string
+ env = append(env, EnvDirectProxy...)
+ env = append(env, EnvGoVCS...)
+ args := []string{"test", pkg}
+ args = append(args, testParams...)
+ out, err := ExecuteCommand(env, "go", args...)
+ if err != nil {
+ for _, line := range strfun.SplitLines(out) {
+ if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") {
+ continue
+ }
+ fmt.Fprintln(os.Stderr, line)
+ }
+ }
+ return err
+}
+func checkGoVet() error {
+ out, err := ExecuteCommand(EnvGoVCS, "go", "vet", "./...")
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Some checks failed")
+ if len(out) > 0 {
+ fmt.Fprintln(os.Stderr, out)
+ }
+ }
+ return err
+}
+
+func checkShadow(forRelease bool) error {
+ path, err := findExecStrict("shadow", forRelease)
+ if path == "" {
+ return err
+ }
+ out, err := ExecuteCommand(EnvGoVCS, path, "-strict", "./...")
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Some shadowed variables found")
+ if len(out) > 0 {
+ fmt.Fprintln(os.Stderr, out)
+ }
+ }
+ return err
+}
+
+func checkStaticcheck() error {
+ out, err := ExecuteCommand(EnvGoVCS, "staticcheck", "./...")
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Some staticcheck problems found")
+ if len(out) > 0 {
+ fmt.Fprintln(os.Stderr, out)
+ }
+ }
+ return err
+}
+
+func checkUnparam(forRelease bool) error {
+ path, err := findExecStrict("unparam", forRelease)
+ if path == "" {
+ return err
+ }
+ out, err := ExecuteCommand(EnvGoVCS, path, "./...")
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Some unparam problems found")
+ if len(out) > 0 {
+ fmt.Fprintln(os.Stderr, out)
+ }
+ }
+ if forRelease {
+ if out2, err2 := ExecuteCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil {
+ fmt.Fprintln(os.Stderr, "Some optional unparam problems found")
+ if len(out2) > 0 {
+ fmt.Fprintln(os.Stderr, out2)
+ }
+ }
+ }
+ return err
+}
+
+func checkGoVulncheck() error {
+ out, err := ExecuteCommand(EnvGoVCS, "govulncheck", "./...")
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Some checks failed")
+ if len(out) > 0 {
+ fmt.Fprintln(os.Stderr, out)
+ }
+ }
+ return err
+}
+func findExec(cmd string) string {
+ if path, err := ExecuteCommand(nil, "which", cmd); err == nil && path != "" {
+ return strings.TrimSpace(path)
+ }
+ return ""
+}
+
+func findExecStrict(cmd string, forRelease bool) (string, error) {
+ path := findExec(cmd)
+ if path != "" || !forRelease {
+ return path, nil
+ }
+ return "", errors.New("Command '" + cmd + "' not installed, but required for release")
+}
+
+func checkFossilExtra() error {
+ out, err := ExecuteCommand(nil, "fossil", "extra")
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'")
+ return err
+ }
+ if len(out) > 0 {
+ fmt.Fprint(os.Stderr, "Warning: unversioned file(s):")
+ for i, extra := range strfun.SplitLines(out) {
+ if i > 0 {
+ fmt.Fprint(os.Stderr, ",")
+ }
+ fmt.Fprintf(os.Stderr, " %q", extra)
+ }
+ fmt.Fprintln(os.Stderr)
+ }
+ return nil
+}
Index: usecase/authenticate.go
==================================================================
--- usecase/authenticate.go
+++ usecase/authenticate.go
@@ -1,53 +1,47 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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"
+ "math/rand/v2"
"net/http"
"time"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/auth"
"zettelstore.de/z/auth/cred"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
"zettelstore.de/z/logger"
- "zettelstore.de/z/search"
-)
-
-// AuthenticatePort is the interface used by this use case.
-type AuthenticatePort interface {
- GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
- SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
-}
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
// Authenticate is the data for this use case.
type Authenticate struct {
log *logger.Logger
token auth.TokenManager
- port AuthenticatePort
- ucGetUser GetUser
+ ucGetUser *GetUser
}
// NewAuthenticate creates a new use case.
-func NewAuthenticate(log *logger.Logger, token auth.TokenManager, authz auth.AuthzManager, port AuthenticatePort) Authenticate {
+func NewAuthenticate(log *logger.Logger, token auth.TokenManager, ucGetUser *GetUser) Authenticate {
return Authenticate{
log: log,
token: token,
- port: port,
- ucGetUser: NewGetUser(authz, port),
+ ucGetUser: ucGetUser,
}
}
// Run executes the use case.
//
@@ -94,11 +88,11 @@
// 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
+ 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)
}
@@ -137,15 +131,15 @@
)
// Run executes the use case.
func (uc *IsAuthenticated) Run(ctx context.Context) IsAuthenticatedResult {
if !uc.authz.WithAuth() {
- uc.log.Sense().Str("auth", "disabled").Msg("IsAuthenticated")
+ uc.log.Info().Str("auth", "disabled").Msg("IsAuthenticated")
return IsAuthenticatedDisabled
}
if uc.port.GetUser(ctx) == nil {
- uc.log.Sense().Msg("IsAuthenticated is false")
+ uc.log.Info().Msg("IsAuthenticated is false")
return IsAuthenticatedAndInvalid
}
- uc.log.Sense().Msg("IsAuthenticated is true")
+ uc.log.Info().Msg("IsAuthenticated is true")
return IsAuthenticatedAndValid
}
DELETED usecase/context.go
Index: usecase/context.go
==================================================================
--- usecase/context.go
+++ usecase/context.go
@@ -1,197 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 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.
-//-----------------------------------------------------------------------------
-
-package usecase
-
-import (
- "context"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
-)
-
-// ZettelContextPort is the interface used by this use case.
-type ZettelContextPort interface {
- // GetMeta retrieves just the meta data of a specific zettel.
- GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
-}
-
-// ZettelContextConfig is the interface to allow the usecase to read some config data.
-type ZettelContextConfig interface {
- // GetHomeZettel returns the value of the "home-zettel" key.
- GetHomeZettel() id.Zid
-}
-
-// ZettelContext is the data for this use case.
-type ZettelContext struct {
- port ZettelContextPort
- config ZettelContextConfig
-}
-
-// NewZettelContext creates a new use case.
-func NewZettelContext(port ZettelContextPort, config ZettelContextConfig) ZettelContext {
- return ZettelContext{port: port, config: config}
-}
-
-// ZettelContextDirection determines the way, the context is calculated.
-type ZettelContextDirection int
-
-// Constant values for ZettelContextDirection
-const (
- _ ZettelContextDirection = iota
- ZettelContextForward // Traverse all forwarding links
- ZettelContextBackward // Traverse all backwaring links
- ZettelContextBoth // Traverse both directions
-)
-
-// Run executes the use case.
-func (uc ZettelContext) Run(ctx context.Context, zid id.Zid, dir ZettelContextDirection, depth, limit int) (result []*meta.Meta, err error) {
- start, err := uc.port.GetMeta(ctx, zid)
- if err != nil {
- return nil, err
- }
- tasks := newQueue(start, depth, limit, uc.config.GetHomeZettel())
- isBackward := dir == ZettelContextBoth || dir == ZettelContextBackward
- isForward := dir == ZettelContextBoth || dir == ZettelContextForward
- for {
- m, curDepth, found := tasks.next()
- if !found {
- break
- }
- result = append(result, m)
-
- for _, p := range m.ComputedPairsRest() {
- tasks.addPair(ctx, uc.port, p.Key, p.Value, curDepth+1, isBackward, isForward)
- }
- }
- return result, nil
-}
-
-type ztlCtxTask struct {
- next *ztlCtxTask
- meta *meta.Meta
- depth int
-}
-
-type contextQueue struct {
- home id.Zid
- seen id.Set
- first *ztlCtxTask
- last *ztlCtxTask
- maxDepth int
- limit int
-}
-
-func newQueue(m *meta.Meta, maxDepth, limit int, home id.Zid) *contextQueue {
- task := &ztlCtxTask{
- next: nil,
- meta: m,
- depth: 0,
- }
- result := &contextQueue{
- home: home,
- seen: id.NewSet(),
- first: task,
- last: task,
- maxDepth: maxDepth,
- limit: limit,
- }
- return result
-}
-
-func (zc *contextQueue) addPair(
- ctx context.Context, port ZettelContextPort,
- key, value string,
- curDepth int, isBackward, isForward bool,
-) {
- if key == api.KeyBackward {
- if isBackward {
- zc.addIDSet(ctx, port, curDepth, value)
- }
- return
- }
- if key == api.KeyForward {
- if isForward {
- zc.addIDSet(ctx, port, curDepth, value)
- }
- return
- }
- if key == api.KeyBack {
- return
- }
- hasInverse := meta.Inverse(key) != ""
- if (!hasInverse || !isBackward) && (hasInverse || !isForward) {
- return
- }
- if t := meta.Type(key); t == meta.TypeID {
- zc.addID(ctx, port, curDepth, value)
- } else if t == meta.TypeIDSet {
- zc.addIDSet(ctx, port, curDepth, value)
- }
-}
-
-func (zc *contextQueue) addID(ctx context.Context, port ZettelContextPort, depth int, value string) {
- if (zc.maxDepth > 0 && depth > zc.maxDepth) || zc.hasLimit() {
- return
- }
-
- zid, err := id.Parse(value)
- if err != nil || zid == zc.home {
- return
- }
-
- m, err := port.GetMeta(ctx, zid)
- if err != nil {
- return
- }
-
- task := &ztlCtxTask{next: nil, meta: m, depth: depth}
- if zc.first == nil {
- zc.first = task
- zc.last = task
- } else {
- zc.last.next = task
- zc.last = task
- }
-}
-
-func (zc *contextQueue) addIDSet(ctx context.Context, port ZettelContextPort, curDepth int, value string) {
- for _, val := range meta.ListFromValue(value) {
- zc.addID(ctx, port, curDepth, val)
- }
-}
-
-func (zc *contextQueue) next() (*meta.Meta, int, bool) {
- if zc.hasLimit() {
- return nil, -1, false
- }
- for zc.first != nil {
- task := zc.first
- zc.first = task.next
- if zc.first == nil {
- zc.last = nil
- }
- m := task.meta
- zid := m.Zid
- _, found := zc.seen[zid]
- if found {
- continue
- }
- zc.seen.Zid(zid)
- return m, task.depth, true
- }
- return nil, -1, false
-}
-
-func (zc *contextQueue) hasLimit() bool {
- limit := zc.limit
- return limit > 0 && len(zc.seen) > limit
-}
Index: usecase/create_zettel.go
==================================================================
--- usecase/create_zettel.go
+++ usecase/create_zettel.go
@@ -1,32 +1,36 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/config"
- "zettelstore.de/z/domain"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
"zettelstore.de/z/logger"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
)
// CreateZettelPort is the interface used by this use case.
type CreateZettelPort interface {
// CreateZettel creates a new zettel.
- CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error)
+ CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error)
}
// CreateZettel is the data for this use case.
type CreateZettel struct {
log *logger.Logger
@@ -42,83 +46,113 @@
port: port,
}
}
// PrepareCopy the zettel for further modification.
-func (*CreateZettel) PrepareCopy(origZettel domain.Zettel) domain.Zettel {
- m := origZettel.Meta.Clone()
- if title, found := m.Get(api.KeyTitle); found {
+func (*CreateZettel) PrepareCopy(origZettel zettel.Zettel) zettel.Zettel {
+ origMeta := origZettel.Meta
+ m := origMeta.Clone()
+ if title, found := origMeta.Get(api.KeyTitle); found {
m.Set(api.KeyTitle, prependTitle(title, "Copy", "Copy of "))
}
- if readonly, found := m.Get(api.KeyReadOnly); found {
- m.Set(api.KeyReadOnly, copyReadonly(readonly))
- }
+ 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(api.KeyPredecessor, origMeta.Zid.String())
+ setReadonly(m)
content := origZettel.Content
content.TrimSpace()
- return domain.Zettel{Meta: m, Content: content}
+ return zettel.Zettel{Meta: m, Content: content}
}
// PrepareFolge the zettel for further modification.
-func (*CreateZettel) PrepareFolge(origZettel domain.Zettel) domain.Zettel {
+func (*CreateZettel) PrepareFolge(origZettel zettel.Zettel) zettel.Zettel {
origMeta := origZettel.Meta
m := meta.New(id.Invalid)
if title, found := origMeta.Get(api.KeyTitle); found {
m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of "))
}
- m.SetNonEmpty(api.KeyRole, origMeta.GetDefault(api.KeyRole, ""))
- m.SetNonEmpty(api.KeyTags, origMeta.GetDefault(api.KeyTags, ""))
- m.SetNonEmpty(api.KeySyntax, origMeta.GetDefault(api.KeySyntax, ""))
+ updateMetaRoleTagsSyntax(m, origMeta)
m.Set(api.KeyPrecursor, origMeta.Zid.String())
- return domain.Zettel{Meta: m, Content: domain.NewContent(nil)}
+ return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
+}
+
+// PrepareChild the zettel for further modification.
+func (*CreateZettel) PrepareChild(origZettel zettel.Zettel) zettel.Zettel {
+ origMeta := origZettel.Meta
+ m := meta.New(id.Invalid)
+ if title, found := origMeta.Get(api.KeyTitle); found {
+ m.Set(api.KeyTitle, prependTitle(title, "Child", "Child of "))
+ }
+ updateMetaRoleTagsSyntax(m, origMeta)
+ m.Set(api.KeySuperior, origMeta.Zid.String())
+ return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}
// PrepareNew the zettel for further modification.
-func (*CreateZettel) PrepareNew(origZettel domain.Zettel) domain.Zettel {
+func (*CreateZettel) PrepareNew(origZettel zettel.Zettel, newTitle string) zettel.Zettel {
m := meta.New(id.Invalid)
om := origZettel.Meta
m.SetNonEmpty(api.KeyTitle, om.GetDefault(api.KeyTitle, ""))
- m.SetNonEmpty(api.KeyRole, om.GetDefault(api.KeyRole, ""))
- m.SetNonEmpty(api.KeyTags, om.GetDefault(api.KeyTags, ""))
- m.SetNonEmpty(api.KeySyntax, om.GetDefault(api.KeySyntax, ""))
+ updateMetaRoleTagsSyntax(m, om)
const prefixLen = len(meta.NewPrefix)
for _, pair := range om.PairsRest() {
if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix {
m.Set(key[prefixLen:], pair.Value)
}
}
+ if newTitle != "" {
+ m.Set(api.KeyTitle, newTitle)
+ }
content := origZettel.Content
content.TrimSpace()
- return domain.Zettel{Meta: m, Content: content}
+ return zettel.Zettel{Meta: m, Content: content}
+}
+
+func updateMetaRoleTagsSyntax(m, orig *meta.Meta) {
+ m.SetNonEmpty(api.KeyRole, orig.GetDefault(api.KeyRole, ""))
+ m.SetNonEmpty(api.KeyTags, orig.GetDefault(api.KeyTags, ""))
+ m.SetNonEmpty(api.KeySyntax, orig.GetDefault(api.KeySyntax, meta.DefaultSyntax))
}
func prependTitle(title, s0, s1 string) string {
if len(title) > 0 {
return s1 + title
}
return s0
}
-func copyReadonly(string) string {
- // Currently, "false" is a safe value.
- //
- // If the current user and its role is known, a mor elaborative calculation
- // could be done: set it to a value, so that the current user will be able
- // to modify it later.
- return api.ValueFalse
+func setReadonly(m *meta.Meta) {
+ if _, found := m.Get(api.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(api.KeyReadOnly, api.ValueFalse)
+ }
}
// Run executes the use case.
-func (uc *CreateZettel) Run(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
+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(api.KeyCreated, time.Now().Local().Format(id.TimestampLayout))
m.Delete(api.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
}
Index: usecase/delete_zettel.go
==================================================================
--- usecase/delete_zettel.go
+++ usecase/delete_zettel.go
@@ -1,22 +1,25 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2021 Detlef Stern
+// Copyright (c) 2020-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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"
- "zettelstore.de/z/domain/id"
"zettelstore.de/z/logger"
+ "zettelstore.de/z/zettel/id"
)
// DeleteZettelPort is the interface used by this use case.
type DeleteZettelPort interface {
// DeleteZettel removes the zettel from the box.
Index: usecase/evaluate.go
==================================================================
--- usecase/evaluate.go
+++ usecase/evaluate.go
@@ -1,86 +1,86 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 Detlef Stern
+// 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/ast"
"zettelstore.de/z/config"
- "zettelstore.de/z/domain"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
"zettelstore.de/z/evaluator"
"zettelstore.de/z/parser"
- "zettelstore.de/z/search"
+ "zettelstore.de/z/query"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
)
// Evaluate is the data for this use case.
type Evaluate struct {
- rtConfig config.Config
- getZettel GetZettel
- getMeta GetMeta
- listMeta ListMeta
+ rtConfig config.Config
+ ucGetZettel *GetZettel
+ ucQuery *Query
}
// NewEvaluate creates a new use case.
-func NewEvaluate(rtConfig config.Config, getZettel GetZettel, getMeta GetMeta, listMeta ListMeta) Evaluate {
+func NewEvaluate(rtConfig config.Config, ucGetZettel *GetZettel, ucQuery *Query) Evaluate {
return Evaluate{
- rtConfig: rtConfig,
- getZettel: getZettel,
- getMeta: getMeta,
- listMeta: listMeta,
+ 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.getZettel.Run(ctx, zid)
+ zettel, err := uc.ucGetZettel.Run(ctx, zid)
if err != nil {
return nil, err
}
- zn, err := parser.ParseZettel(zettel, syntax, uc.rtConfig), nil
- 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, nil
+ 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
}
// RunMetadata executes the use case for a metadata value.
func (uc *Evaluate) RunMetadata(ctx context.Context, value string) ast.InlineSlice {
is := parser.ParseMetadata(value)
evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is)
return is
}
-// RunMetadataNoLink executes the use case for a metadata value, but ignores link and footnote nodes.
-func (uc *Evaluate) RunMetadataNoLink(ctx context.Context, value string) ast.InlineSlice {
- is := parser.ParseMetadataNoLink(value)
- evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is)
- return is
-}
-
-// GetMeta retrieves the metadata of a given zettel identifier.
-func (uc *Evaluate) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
- return uc.getMeta.Run(ctx, zid)
-}
-
-// GetZettel retrieves the full zettel of a given zettel identifier.
-func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
- return uc.getZettel.Run(ctx, zid)
-}
-
-// SelectMeta returns a list of metadata that comply to the given selection criteria.
-func (uc *Evaluate) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) {
- return uc.listMeta.Run(ctx, s)
+// 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)
}
DELETED usecase/get_all_meta.go
Index: usecase/get_all_meta.go
==================================================================
--- usecase/get_all_meta.go
+++ usecase/get_all_meta.go
@@ -1,39 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2021 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.
-//-----------------------------------------------------------------------------
-
-package usecase
-
-import (
- "context"
-
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
-)
-
-// GetAllMetaPort is the interface used by this use case.
-type GetAllMetaPort interface {
- // GetAllMeta retrieves just the meta data of a specific zettel.
- GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error)
-}
-
-// GetAllMeta is the data for this use case.
-type GetAllMeta struct {
- port GetAllMetaPort
-}
-
-// NewGetAllMeta creates a new use case.
-func NewGetAllMeta(port GetAllMetaPort) GetAllMeta {
- return GetAllMeta{port: port}
-}
-
-// Run executes the use case.
-func (uc GetAllMeta) Run(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) {
- return uc.port.GetAllMeta(ctx, zid)
-}
ADDED usecase/get_all_zettel.go
Index: usecase/get_all_zettel.go
==================================================================
--- usecase/get_all_zettel.go
+++ usecase/get_all_zettel.go
@@ -0,0 +1,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"
+
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+)
+
+// 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)
+}
DELETED usecase/get_meta.go
Index: usecase/get_meta.go
==================================================================
--- usecase/get_meta.go
+++ usecase/get_meta.go
@@ -1,39 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2021 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.
-//-----------------------------------------------------------------------------
-
-package usecase
-
-import (
- "context"
-
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
-)
-
-// GetMetaPort is the interface used by this use case.
-type GetMetaPort interface {
- // GetMeta retrieves just the meta data of a specific zettel.
- GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
-}
-
-// GetMeta is the data for this use case.
-type GetMeta struct {
- port GetMetaPort
-}
-
-// NewGetMeta creates a new use case.
-func NewGetMeta(port GetMetaPort) GetMeta {
- return GetMeta{port: port}
-}
-
-// Run executes the use case.
-func (uc GetMeta) Run(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
- return uc.port.GetMeta(ctx, zid)
-}
ADDED usecase/get_special_zettel.go
Index: usecase/get_special_zettel.go
==================================================================
--- usecase/get_special_zettel.go
+++ usecase/get_special_zettel.go
@@ -0,0 +1,113 @@
+//-----------------------------------------------------------------------------
+// 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"
+ "zettelstore.de/z/query"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
+
+// 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 string) (zettel.Zettel, error) {
+ tag = meta.NormalizeTag(tag)
+ q := query.Parse(
+ api.KeyTitle + api.SearchOperatorEqual + tag + " " +
+ api.KeyRole + api.SearchOperatorHas + api.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 string }
+
+func (etznf ErrTagZettelNotFound) Error() string { return "tag zettel not found: " + 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 string) (zettel.Zettel, error) {
+ q := query.Parse(
+ api.KeyTitle + api.SearchOperatorEqual + role + " " +
+ api.KeyRole + api.SearchOperatorHas + api.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 string }
+
+func (etznf ErrRoleZettelNotFound) Error() string { return "role zettel not found: " + etznf.Role }
Index: usecase/get_user.go
==================================================================
--- usecase/get_user.go
+++ usecase/get_user.go
@@ -1,35 +1,39 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2021 Detlef Stern
+// Copyright (c) 2020-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/auth"
"zettelstore.de/z/box"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
- "zettelstore.de/z/search"
+ "zettelstore.de/z/query"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
)
// Use case: return user identified by meta key ident.
// ---------------------------------------------------
// GetUserPort is the interface used by this use case.
type GetUserPort interface {
- GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
- SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
+ 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
@@ -46,19 +50,17 @@
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.
- identMeta, err := uc.port.GetMeta(ctx, uc.authz.Owner())
- if err == nil && identMeta.GetDefault(api.KeyUserID, "") == ident {
- return identMeta, nil
+ identZettel, err := uc.port.GetZettel(ctx, uc.authz.Owner())
+ if err == nil && identZettel.Meta.GetDefault(api.KeyUserID, "") == ident {
+ return identZettel.Meta, nil
}
// Owner was not found or has another ident. Try via list search.
- var s *search.Search
- s = s.AddExpr("", "="+ident)
- s = s.AddExpr(api.KeyUserID, ident)
- metaList, err := uc.port.SelectMeta(ctx, s)
+ q := query.Parse(api.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
@@ -69,11 +71,11 @@
// 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 {
- GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
+ GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}
// GetUserByZid is the data for this use case.
type GetUserByZid struct {
port GetUserByZidPort
@@ -84,15 +86,16 @@
return GetUserByZid{port: port}
}
// GetUser executes the use case.
func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) {
- userMeta, err := uc.port.GetMeta(box.NoEnrichContext(ctx), zid)
+ userZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), zid)
if err != nil {
return nil, err
}
+ userMeta := userZettel.Meta
if val, ok := userMeta.Get(api.KeyUserID); !ok || val != ident {
return nil, nil
}
return userMeta, nil
}
Index: usecase/get_zettel.go
==================================================================
--- usecase/get_zettel.go
+++ usecase/get_zettel.go
@@ -1,28 +1,31 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2021 Detlef Stern
+// Copyright (c) 2020-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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"
- "zettelstore.de/z/domain"
- "zettelstore.de/z/domain/id"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
)
// GetZettelPort is the interface used by this use case.
type GetZettelPort interface {
// GetZettel retrieves a specific zettel.
- GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
+ GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}
// GetZettel is the data for this use case.
type GetZettel struct {
port GetZettelPort
@@ -32,8 +35,8 @@
func NewGetZettel(port GetZettelPort) GetZettel {
return GetZettel{port: port}
}
// Run executes the use case.
-func (uc GetZettel) Run(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
+func (uc GetZettel) Run(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
return uc.port.GetZettel(ctx, zid)
}
Index: usecase/lists.go
==================================================================
--- usecase/lists.go
+++ usecase/lists.go
@@ -1,54 +1,35 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/box"
- "zettelstore.de/z/domain/meta"
"zettelstore.de/z/parser"
- "zettelstore.de/z/search"
-)
-
-// ListMetaPort is the interface used by this use case.
-type ListMetaPort interface {
- // SelectMeta returns all zettel metadata that match the selection criteria.
- SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
-}
-
-// ListMeta is the data for this use case.
-type ListMeta struct {
- port ListMetaPort
-}
-
-// NewListMeta creates a new use case.
-func NewListMeta(port ListMetaPort) ListMeta {
- return ListMeta{port: port}
-}
-
-// Run executes the use case.
-func (uc ListMeta) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) {
- return uc.port.SelectMeta(ctx, s)
-}
-
-// -------- List roles -------------------------------------------------------
+ "zettelstore.de/z/query"
+ "zettelstore.de/z/zettel/meta"
+)
+
+// -------- List syntax ------------------------------------------------------
// ListSyntaxPort is the interface used by this use case.
type ListSyntaxPort interface {
- // SelectMeta returns all zettel metadata that match the selection criteria.
- SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
+ 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
@@ -59,31 +40,29 @@
return ListSyntax{port: port}
}
// Run executes the use case.
func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) {
- var s *search.Search
- s = s.AddExpr(api.KeySyntax, "") // We look for all metadata with a syntax key
- metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), s)
+ q := query.Parse(api.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, api.KeySyntax)
for _, syn := range parser.GetSyntaxes() {
if _, found := result[syn]; !found {
- result[syn] = nil
+ delete(result, syn)
}
}
return result, nil
}
// -------- List roles -------------------------------------------------------
// ListRolesPort is the interface used by this use case.
type ListRolesPort interface {
- // SelectMeta returns all zettel metadata that match the selection criteria.
- SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
+ 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
@@ -94,50 +73,12 @@
return ListRoles{port: port}
}
// Run executes the use case.
func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) {
- var s *search.Search
- s = s.AddExpr(api.KeyRole, "") // We look for all metadata with a role key
- metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), s)
+ q := query.Parse(api.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, api.KeyRole), nil
}
-
-// -------- List tags --------------------------------------------------------
-
-// ListTagsPort is the interface used by this use case.
-type ListTagsPort interface {
- // SelectMeta returns all zettel metadata that match the selection criteria.
- SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
-}
-
-// ListTags is the data for this use case.
-type ListTags struct {
- port ListTagsPort
-}
-
-// NewListTags creates a new use case.
-func NewListTags(port ListTagsPort) ListTags {
- return ListTags{port: port}
-}
-
-// Run executes the use case.
-func (uc ListTags) Run(ctx context.Context, minCount int) (meta.Arrangement, error) {
- var s *search.Search
- s = s.AddExpr(api.KeyTags, "") // We look for all metadata with a tag
- metas, err := uc.port.SelectMeta(ctx, s)
- if err != nil {
- return nil, err
- }
- result := meta.CreateArrangement(metas, api.KeyAllTags)
- if minCount > 1 {
- for t, ms := range result {
- if len(ms) < minCount {
- delete(result, t)
- }
- }
- }
- return result, nil
-}
DELETED usecase/order.go
Index: usecase/order.go
==================================================================
--- usecase/order.go
+++ usecase/order.go
@@ -1,54 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 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.
-//-----------------------------------------------------------------------------
-
-package usecase
-
-import (
- "context"
-
- "zettelstore.de/z/collect"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
-)
-
-// ZettelOrderPort is the interface used by this use case.
-type ZettelOrderPort interface {
- // GetMeta retrieves just the meta data of a specific zettel.
- GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
-}
-
-// ZettelOrder is the data for this use case.
-type ZettelOrder struct {
- port ZettelOrderPort
- evaluate Evaluate
-}
-
-// NewZettelOrder creates a new use case.
-func NewZettelOrder(port ZettelOrderPort, evaluate Evaluate) ZettelOrder {
- return ZettelOrder{port: port, evaluate: evaluate}
-}
-
-// Run executes the use case.
-func (uc ZettelOrder) Run(ctx context.Context, zid id.Zid, syntax string) (
- start *meta.Meta, result []*meta.Meta, err error,
-) {
- zn, err := uc.evaluate.Run(ctx, zid, syntax)
- if err != nil {
- return nil, nil, err
- }
- for _, ref := range collect.Order(zn) {
- if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil {
- if m, err3 := uc.port.GetMeta(ctx, collectedZid); err3 == nil {
- result = append(result, m)
- }
- }
- }
- return zn.Meta, result, nil
-}
Index: usecase/parse_zettel.go
==================================================================
--- usecase/parse_zettel.go
+++ usecase/parse_zettel.go
@@ -1,24 +1,27 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2021 Detlef Stern
+// Copyright (c) 2020-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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"
"zettelstore.de/z/ast"
"zettelstore.de/z/config"
- "zettelstore.de/z/domain/id"
"zettelstore.de/z/parser"
+ "zettelstore.de/z/zettel/id"
)
// ParseZettel is the data for this use case.
type ParseZettel struct {
rtConfig config.Config
@@ -35,7 +38,7 @@
zettel, err := uc.getZettel.Run(ctx, zid)
if err != nil {
return nil, err
}
- return parser.ParseZettel(zettel, syntax, uc.rtConfig), nil
+ return parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil
}
ADDED usecase/query.go
Index: usecase/query.go
==================================================================
--- usecase/query.go
+++ usecase/query.go
@@ -0,0 +1,279 @@
+//-----------------------------------------------------------------------------
+// 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/api"
+ "zettelstore.de/z/ast"
+ "zettelstore.de/z/box"
+ "zettelstore.de/z/collect"
+ "zettelstore.de/z/parser"
+ "zettelstore.de/z/query"
+ "zettelstore.de/z/strfun"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
+
+// 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, m.GetDefault(api.KeySyntax, meta.DefaultSyntax))
+ if err != nil {
+ continue
+ }
+ for _, ref := range collect.Order(zn) {
+ 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 := id.NewSetCap(len(metaSeq))
+ refZids := id.NewSetCap(len(metaSeq) * 4) // Assumption: there are four zids per zettel
+ for _, m := range metaSeq {
+ metaZids.Add(m.Zid)
+ refZids.Add(m.Zid)
+ for _, pair := range m.ComputedPairsRest() {
+ switch meta.Type(pair.Key) {
+ case meta.TypeID:
+ if zid, errParse := id.Parse(pair.Value); errParse == nil {
+ refZids.Add(zid)
+ }
+ case meta.TypeIDSet:
+ for _, value := range meta.ListFromValue(pair.Value) {
+ if zid, errParse := id.Parse(value); errParse == nil {
+ refZids.Add(zid)
+ }
+ }
+ }
+ }
+ }
+ candidates = filterByZid(candidates, refZids)
+ return uc.filterCandidates(ctx, candidates, words)
+}
+
+func filterByZid(candidates []*meta.Meta, ignoreSeq id.Set) []*meta.Meta {
+ result := make([]*meta.Meta, 0, len(candidates))
+ for _, m := range candidates {
+ if !ignoreSeq.ContainsOrNil(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))
+candLoop:
+ 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)
+
+ for _, pair := range zettel.Meta.Pairs() {
+ if meta.Type(pair.Key) != meta.TypeZettelmarkup {
+ continue
+ }
+ is := uc.ucEvaluate.RunMetadata(ctx, pair.Value)
+ ast.Walk(&v, &is)
+ if v.found {
+ result = append(result, cand)
+ continue candLoop
+ }
+ }
+
+ syntax := zettel.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax)
+ if !parser.IsASTParser(syntax) {
+ continue
+ }
+ zn := uc.ucEvaluate.RunZettel(ctx, zettel, syntax)
+ ast.Walk(&v, &zn.Ast)
+ 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)...)
+ case *ast.SpaceNode:
+ default:
+ if curList != nil {
+ result = append(result, v.joinWords(curList))
+ curList = nil
+ }
+ }
+ }
+ if curList != nil {
+ result = append(result, v.joinWords(curList))
+ }
+ return result
+}
Index: usecase/refresh.go
==================================================================
--- usecase/refresh.go
+++ usecase/refresh.go
@@ -1,13 +1,16 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021 Detlef Stern
+// Copyright (c) 2021-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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 (
ADDED usecase/reindex.go
Index: usecase/reindex.go
==================================================================
--- usecase/reindex.go
+++ usecase/reindex.go
@@ -0,0 +1,44 @@
+//-----------------------------------------------------------------------------
+// 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"
+
+ "zettelstore.de/z/logger"
+ "zettelstore.de/z/zettel/id"
+)
+
+// 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
+}
Index: usecase/rename_zettel.go
==================================================================
--- usecase/rename_zettel.go
+++ usecase/rename_zettel.go
@@ -1,32 +1,32 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2021 Detlef Stern
+// Copyright (c) 2020-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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"
"zettelstore.de/z/box"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
"zettelstore.de/z/logger"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
)
// RenameZettelPort is the interface used by this use case.
type RenameZettelPort interface {
- // GetMeta retrieves just the meta data of a specific zettel.
- GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
-
- // Rename changes the current id to a new id.
+ GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
RenameZettel(ctx context.Context, curZid, newZid id.Zid) error
}
// RenameZettel is the data for this use case.
type RenameZettel struct {
@@ -35,11 +35,11 @@
}
// ErrZidInUse is returned if the zettel id is not appropriate for the box operation.
type ErrZidInUse struct{ Zid id.Zid }
-func (err *ErrZidInUse) Error() string {
+func (err ErrZidInUse) Error() string {
return "Zettel id already in use: " + err.Zid.String()
}
// NewRenameZettel creates a new use case.
func NewRenameZettel(log *logger.Logger, port RenameZettelPort) RenameZettel {
@@ -47,19 +47,19 @@
}
// Run executes the use case.
func (uc *RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error {
noEnrichCtx := box.NoEnrichContext(ctx)
- if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil {
+ if _, err := uc.port.GetZettel(noEnrichCtx, curZid); err != nil {
return err
}
if newZid == curZid {
// Nothing to do
return nil
}
- if _, err := uc.port.GetMeta(noEnrichCtx, newZid); err == nil {
- return &ErrZidInUse{Zid: newZid}
+ if _, err := uc.port.GetZettel(noEnrichCtx, newZid); err == nil {
+ return ErrZidInUse{Zid: newZid}
}
err := uc.port.RenameZettel(ctx, curZid, newZid)
uc.log.Info().User(ctx).Zid(curZid).Err(err).Zid(newZid).Msg("Rename zettel")
return err
}
DELETED usecase/unlinked_refs.go
Index: usecase/unlinked_refs.go
==================================================================
--- usecase/unlinked_refs.go
+++ usecase/unlinked_refs.go
@@ -1,178 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 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.
-//-----------------------------------------------------------------------------
-
-package usecase
-
-import (
- "context"
- "strings"
- "unicode"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/ast"
- "zettelstore.de/z/config"
- "zettelstore.de/z/domain"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
- "zettelstore.de/z/encoder/textenc"
- "zettelstore.de/z/evaluator"
- "zettelstore.de/z/parser"
- "zettelstore.de/z/search"
-)
-
-// UnlinkedReferencesPort is the interface used by this use case.
-type UnlinkedReferencesPort interface {
- GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
- GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
- SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
-}
-
-// UnlinkedReferences is the data for this use case.
-type UnlinkedReferences struct {
- port UnlinkedReferencesPort
- rtConfig config.Config
- encText *textenc.Encoder
-}
-
-// NewUnlinkedReferences creates a new use case.
-func NewUnlinkedReferences(port UnlinkedReferencesPort, rtConfig config.Config) UnlinkedReferences {
- return UnlinkedReferences{
- port: port,
- rtConfig: rtConfig,
- encText: textenc.Create(),
- }
-}
-
-// Run executes the usecase with already evaluated title value.
-func (uc *UnlinkedReferences) Run(ctx context.Context, title string, s *search.Search) ([]*meta.Meta, error) {
- words := makeWords(title)
- if len(words) == 0 {
- return nil, nil
- }
- for _, word := range words {
- s = s.AddExpr("", "="+word)
- }
-
- // Limit applies to the filtering process, not to SelectMeta
- limit := s.GetLimit()
- s = s.SetLimit(0)
-
- candidates, err := uc.port.SelectMeta(ctx, s)
- if err != nil {
- return nil, err
- }
- s = s.SetLimit(limit) // Restore limit
- return s.Limit(uc.filterCandidates(ctx, candidates, words)), nil
-}
-
-func makeWords(text string) []string {
- return strings.FieldsFunc(text, func(r rune) bool {
- return unicode.In(r, unicode.C, unicode.P, unicode.Z)
- })
-}
-
-func (uc *UnlinkedReferences) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta {
- result := make([]*meta.Meta, 0, len(candidates))
-candLoop:
- 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)
-
- for _, pair := range zettel.Meta.Pairs() {
- if meta.Type(pair.Key) != meta.TypeZettelmarkup {
- continue
- }
- is := parser.ParseMetadata(pair.Value)
- evaluator.EvaluateInline(ctx, uc.port, uc.rtConfig, &is)
- ast.Walk(&v, &is)
- if v.found {
- result = append(result, cand)
- continue candLoop
- }
- }
-
- syntax := zettel.Meta.GetDefault(api.KeySyntax, "")
- if !parser.IsTextParser(syntax) {
- continue
- }
- zn, err := parser.ParseZettel(zettel, syntax, nil), nil
- if err != nil {
- continue
- }
- evaluator.EvaluateZettel(ctx, uc.port, uc.rtConfig, zn)
- ast.Walk(&v, &zn.Ast)
- 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, makeWords(n.Text)...)
- case *ast.SpaceNode:
- default:
- if curList != nil {
- result = append(result, v.joinWords(curList))
- curList = nil
- }
- }
- }
- if curList != nil {
- result = append(result, v.joinWords(curList))
- }
- return result
-}
Index: usecase/update_zettel.go
==================================================================
--- usecase/update_zettel.go
+++ usecase/update_zettel.go
@@ -1,34 +1,38 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2021 Detlef Stern
+// Copyright (c) 2020-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/box"
- "zettelstore.de/z/domain"
- "zettelstore.de/z/domain/id"
"zettelstore.de/z/logger"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
)
// UpdateZettelPort is the interface used by this use case.
type UpdateZettelPort interface {
// GetZettel retrieves a specific zettel.
- GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
+ GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
// UpdateZettel updates an existing zettel.
- UpdateZettel(ctx context.Context, zettel domain.Zettel) error
+ UpdateZettel(ctx context.Context, zettel zettel.Zettel) error
}
// UpdateZettel is the data for this use case.
type UpdateZettel struct {
log *logger.Logger
@@ -39,27 +43,36 @@
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 domain.Zettel, hasContent bool) error {
+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(api.KeyCreated); !found {
+ if val, crFound := oldZettel.Meta.Get(api.KeyCreated); crFound {
+ m.Set(api.KeyCreated, val)
+ }
+ }
m.SetNow(api.KeyModified)
+
m.YamlSep = oldZettel.Meta.YamlSep
if m.Zid == id.ConfigurationZid {
- m.Set(api.KeySyntax, api.ValueSyntaxNone)
+ m.Set(api.KeySyntax, meta.SyntaxNone)
}
+
if !hasContent {
zettel.Content = oldZettel.Content
}
zettel.Content.TrimSpace()
err = uc.port.UpdateZettel(ctx, zettel)
- uc.log.Sense().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel")
+ uc.log.Info().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel")
return err
}
Index: usecase/usecase.go
==================================================================
--- usecase/usecase.go
+++ usecase/usecase.go
@@ -1,12 +1,15 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021 Detlef Stern
+// Copyright (c) 2021-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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
Index: usecase/version.go
==================================================================
--- usecase/version.go
+++ usecase/version.go
@@ -1,13 +1,16 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2022 Detlef Stern
+// 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 (
ADDED web/adapter/adapter.go
Index: web/adapter/adapter.go
==================================================================
--- web/adapter/adapter.go
+++ web/adapter/adapter.go
@@ -0,0 +1,49 @@
+//-----------------------------------------------------------------------------
+// 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"
+ "zettelstore.de/z/usecase"
+ "zettelstore.de/z/zettel/meta"
+)
+
+// 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
+}
Index: web/adapter/api/api.go
==================================================================
--- web/adapter/api/api.go
+++ web/adapter/api/api.go
@@ -1,13 +1,16 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 Detlef Stern
+// 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
@@ -15,89 +18,77 @@
"bytes"
"context"
"net/http"
"time"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/auth"
"zettelstore.de/z/config"
- "zettelstore.de/z/domain/meta"
"zettelstore.de/z/kernel"
"zettelstore.de/z/logger"
"zettelstore.de/z/web/adapter"
"zettelstore.de/z/web/server"
+ "zettelstore.de/z/zettel/meta"
)
// 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
- auth server.Auth
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,
- auth server.Auth, rtConfig config.Config, pol auth.Policy) *API {
+ rtConfig config.Config, pol auth.Policy) *API {
a := &API{
log: log,
b: b,
authz: authz,
token: token,
- auth: auth,
rtConfig: rtConfig,
policy: pol,
tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration),
}
return a
}
-// GetURLPrefix returns the configured URL prefix of the web server.
-func (a *API) GetURLPrefix() string { return a.b.GetURLPrefix() }
-
// 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 a.auth.GetAuthData(ctx)
+ 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.KindJSON)
+ 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.IfErr(err).Msg(text)
+ 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 {
- if buf.Len() == 0 {
- w.WriteHeader(http.StatusNoContent)
- return nil
- }
- adapter.PrepareHeader(w, contentType)
- w.WriteHeader(http.StatusOK)
- _, err := w.Write(buf.Bytes())
- return err
+ return adapter.WriteData(w, buf.Bytes(), contentType)
}
func (a *API) getRights(ctx context.Context, m *meta.Meta) (result api.ZettelRights) {
pol := a.policy
- user := a.auth.GetUser(ctx)
+ user := server.GetUser(ctx)
if pol.CanCreate(user, m) {
result |= api.ZettelCanCreate
}
if pol.CanRead(user, m) {
result |= api.ZettelCanRead
Index: web/adapter/api/command.go
==================================================================
--- web/adapter/api/command.go
+++ web/adapter/api/command.go
@@ -1,22 +1,25 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 Detlef Stern
+// 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"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/usecase"
)
// MakePostCommandHandler creates a new HTTP handler to execute certain commands.
func (a *API) MakePostCommandHandler(
@@ -23,13 +26,11 @@
ucIsAuth *usecase.IsAuthenticated,
ucRefresh *usecase.Refresh,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- q := r.URL.Query()
- cmd := q.Get(api.QueryKeyCommand)
- switch api.Command(cmd) {
+ switch api.Command(r.URL.Query().Get(api.QueryKeyCommand)) {
case api.CommandAuthenticated:
handleIsAuthenticated(ctx, w, ucIsAuth)
return
case api.CommandRefresh:
err := ucRefresh.Run(ctx)
DELETED web/adapter/api/content_type.go
Index: web/adapter/api/content_type.go
==================================================================
--- web/adapter/api/content_type.go
+++ web/adapter/api/content_type.go
@@ -1,60 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 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.
-//-----------------------------------------------------------------------------
-
-// Package api provides api handlers for web requests.
-package api
-
-import "zettelstore.de/c/api"
-
-const (
- ctHTML = "text/html; charset=utf-8"
- ctJSON = "application/json"
- ctPlainText = "text/plain; charset=utf-8"
- ctSVG = "image/svg+xml"
-)
-
-var mapEncoding2CT = map[api.EncodingEnum]string{
- api.EncoderHTML: ctHTML,
- api.EncoderSexpr: ctPlainText,
- api.EncoderText: ctPlainText,
- api.EncoderZJSON: ctJSON,
- api.EncoderZmk: ctPlainText,
-}
-
-func encoding2ContentType(enc api.EncodingEnum) string {
- if ct, ok := mapEncoding2CT[enc]; ok {
- return ct
- }
- return "application/octet-stream"
-}
-
-var mapSyntax2CT = map[string]string{
- "css": "text/css; charset=utf-8",
- api.ValueSyntaxGif: "image/gif",
- api.ValueSyntaxHTML: "text/html; charset=utf-8",
- "jpeg": "image/jpeg",
- "jpg": "image/jpeg",
- "js": "text/javascript; charset=utf-8",
- "pdf": "application/pdf",
- "png": "image/png",
- api.ValueSyntaxSVG: ctSVG,
- "xml": "text/xml; charset=utf-8",
- api.ValueSyntaxZmk: "text/x-zmk; charset=utf-8",
- "plain": ctPlainText,
- api.ValueSyntaxText: ctPlainText,
- "markdown": "text/markdown; charset=utf-8",
- "md": "text/markdown; charset=utf-8",
- "mustache": ctPlainText,
-}
-
-func syntax2contentType(syntax string) (string, bool) {
- contentType, ok := mapSyntax2CT[syntax]
- return contentType, ok
-}
Index: web/adapter/api/create_zettel.go
==================================================================
--- web/adapter/api/create_zettel.go
+++ web/adapter/api/create_zettel.go
@@ -1,78 +1,78 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021 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.
-//-----------------------------------------------------------------------------
-
-package api
-
-import (
- "bytes"
- "net/http"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/usecase"
- "zettelstore.de/z/web/adapter"
-)
-
-// MakePostCreatePlainZettelHandler creates a new HTTP handler to store content of
-// an existing zettel.
-func (a *API) MakePostCreatePlainZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- zettel, err := buildZettelFromPlainData(r, id.Invalid)
- if err != nil {
- a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error()))
- return
- }
-
- newZid, err := createZettel.Run(ctx, zettel)
- if err != nil {
- a.reportUsecaseError(w, err)
- return
- }
- u := a.NewURLBuilder('z').SetZid(api.ZettelID(newZid.String())).String()
- h := adapter.PrepareHeader(w, ctPlainText)
- h.Set(api.HeaderLocation, u)
- w.WriteHeader(http.StatusCreated)
- _, err = w.Write(newZid.Bytes())
- a.log.IfErr(err).Zid(newZid).Msg("Create Plain Zettel")
- }
-}
-
-// MakePostCreateZettelHandler creates a new HTTP handler to store content of
-// an existing zettel.
-func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- zettel, err := buildZettelFromJSONData(r, id.Invalid)
- if err != nil {
- a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error()))
- return
- }
-
- newZid, err := createZettel.Run(ctx, zettel)
- if err != nil {
- a.reportUsecaseError(w, err)
- return
- }
- var buf bytes.Buffer
- err = encodeJSONData(&buf, api.ZidJSON{ID: api.ZettelID(newZid.String())})
- if err != nil {
- a.log.Fatal().Err(err).Zid(newZid).Msg("Unable to store new Zid in buffer")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
-
- h := adapter.PrepareHeader(w, ctJSON)
- h.Set(api.HeaderLocation, a.NewURLBuilder('j').SetZid(api.ZettelID(newZid.String())).String())
- w.WriteHeader(http.StatusCreated)
- _, err = w.Write(buf.Bytes())
- a.log.IfErr(err).Zid(newZid).Msg("Create JSON Zettel")
+// 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"
+ "zettelstore.de/z/usecase"
+ "zettelstore.de/z/web/adapter"
+ "zettelstore.de/z/web/content"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+)
+
+// MakePostCreateZettelHandler creates a new HTTP handler to store content of
+// an existing zettel.
+func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
+ return 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.ZettelID())
+ 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")
+ }
}
}
Index: web/adapter/api/delete_zettel.go
==================================================================
--- web/adapter/api/delete_zettel.go
+++ web/adapter/api/delete_zettel.go
@@ -1,22 +1,25 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021 Detlef Stern
+// Copyright (c) 2021-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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"
- "zettelstore.de/z/domain/id"
"zettelstore.de/z/usecase"
+ "zettelstore.de/z/zettel/id"
)
// MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
Index: web/adapter/api/get_data.go
==================================================================
--- web/adapter/api/get_data.go
+++ web/adapter/api/get_data.go
@@ -1,43 +1,39 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2022 Detlef Stern
+// 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"
"net/http"
- "zettelstore.de/c/api"
+ "t73f.de/r/sx"
"zettelstore.de/z/usecase"
+ "zettelstore.de/z/zettel/id"
)
// MakeGetDataHandler creates a new HTTP handler to return zettelstore data.
func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
version := ucVersion.Run()
- result := api.VersionJSON{
- Major: version.Major,
- Minor: version.Minor,
- Patch: version.Patch,
- Info: version.Info,
- Hash: version.Hash,
- }
- var buf bytes.Buffer
- err := encodeJSONData(&buf, result)
+ 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.Fatal().Err(err).Msg("Unable to version info in buffer")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
+ a.log.Error().Err(err).Msg("Write Version Info")
}
-
- err = writeBuffer(w, &buf, ctJSON)
- a.log.IfErr(err).Msg("Write Version Info")
}
}
DELETED web/adapter/api/get_eval_zettel.go
Index: web/adapter/api/get_eval_zettel.go
==================================================================
--- web/adapter/api/get_eval_zettel.go
+++ web/adapter/api/get_eval_zettel.go
@@ -1,46 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 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.
-//-----------------------------------------------------------------------------
-
-package api
-
-import (
- "net/http"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/ast"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/encoder"
- "zettelstore.de/z/usecase"
-)
-
-// MakeGetEvalZettelHandler creates a new HTTP handler to return a evaluated zettel.
-func (a *API) MakeGetEvalZettelHandler(evaluate usecase.Evaluate) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- zid, err := id.Parse(r.URL.Path[1:])
- if err != nil {
- http.NotFound(w, r)
- return
- }
-
- ctx := r.Context()
- q := r.URL.Query()
- enc, encStr := getEncoding(r, q, encoder.GetDefaultEncoding())
- part := getPart(q, partContent)
- zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
- if err != nil {
- a.reportUsecaseError(w, err)
- return
- }
- evalMeta := func(value string) ast.InlineSlice {
- return evaluate.RunMetadata(ctx, value)
- }
- a.writeEncodedZettelPart(w, zn, evalMeta, enc, encStr, part)
- }
-}
DELETED web/adapter/api/get_lists.go
Index: web/adapter/api/get_lists.go
==================================================================
--- web/adapter/api/get_lists.go
+++ web/adapter/api/get_lists.go
@@ -1,72 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 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.
-//-----------------------------------------------------------------------------
-
-// Package api provides api handlers for web requests.
-package api
-
-import (
- "bytes"
- "net/http"
- "strconv"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/domain/meta"
- "zettelstore.de/z/usecase"
-)
-
-// MakeListMapMetaHandler creates a new HTTP handler to retrieve mappings of
-// metadata values of a specific key to the list of zettel IDs, which contain
-// this value.
-func (a *API) MakeListMapMetaHandler(listRole usecase.ListRoles, listTags usecase.ListTags) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- var ar meta.Arrangement
- query := r.URL.Query()
- iMinCount, err := strconv.Atoi(query.Get(api.QueryKeyMin))
- if err != nil || iMinCount < 0 {
- iMinCount = 0
- }
- ctx := r.Context()
- key := query.Get(api.QueryKeyKey)
- switch key {
- case api.KeyRole:
- ar, err = listRole.Run(ctx)
- case api.KeyTags:
- ar, err = listTags.Run(ctx, iMinCount)
- default:
- a.log.Info().Str("key", key).Msg("illegal key for retrieving meta map")
- http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
- return
- }
- if err != nil {
- a.reportUsecaseError(w, err)
- return
- }
-
- mm := make(api.MapMeta, len(ar))
- for tag, metaList := range ar {
- zidList := make([]api.ZettelID, 0, len(metaList))
- for _, m := range metaList {
- zidList = append(zidList, api.ZettelID(m.Zid.String()))
- }
- mm[tag] = zidList
- }
-
- var buf bytes.Buffer
- err = encodeJSONData(&buf, api.MapListJSON{Map: mm})
- if err != nil {
- a.log.Fatal().Err(err).Msg("Unable to store map list in buffer")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
-
- err = writeBuffer(w, &buf, ctJSON)
- a.log.IfErr(err).Str("key", key).Msg("write meta map")
- }
-}
DELETED web/adapter/api/get_order.go
Index: web/adapter/api/get_order.go
==================================================================
--- web/adapter/api/get_order.go
+++ web/adapter/api/get_order.go
@@ -1,41 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 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.
-//-----------------------------------------------------------------------------
-
-// Package api provides api handlers for web requests.
-package api
-
-import (
- "net/http"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/usecase"
-)
-
-// MakeGetOrderHandler creates a new API handler to return zettel references
-// of a given zettel.
-func (a *API) MakeGetOrderHandler(zettelOrder usecase.ZettelOrder) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- zid, err := id.Parse(r.URL.Path[1:])
- if err != nil {
- http.NotFound(w, r)
- return
- }
- ctx := r.Context()
- q := r.URL.Query()
- start, metas, err := zettelOrder.Run(ctx, zid, q.Get(api.KeySyntax))
- if err != nil {
- a.reportUsecaseError(w, err)
- return
- }
- err = a.writeMetaList(ctx, w, start, metas)
- a.log.IfErr(err).Zid(zid).Msg("Write Zettel Order")
- }
-}
DELETED web/adapter/api/get_parsed_zettel.go
Index: web/adapter/api/get_parsed_zettel.go
==================================================================
--- web/adapter/api/get_parsed_zettel.go
+++ web/adapter/api/get_parsed_zettel.go
@@ -1,81 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 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.
-//-----------------------------------------------------------------------------
-
-// Package api provides api handlers for web requests.
-package api
-
-import (
- "bytes"
- "fmt"
- "net/http"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/ast"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/encoder"
- "zettelstore.de/z/parser"
- "zettelstore.de/z/usecase"
- "zettelstore.de/z/web/adapter"
-)
-
-// MakeGetParsedZettelHandler creates a new HTTP handler to return a parsed zettel.
-func (a *API) MakeGetParsedZettelHandler(parseZettel usecase.ParseZettel) http.HandlerFunc {
- return 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()
- enc, encStr := getEncoding(r, q, encoder.GetDefaultEncoding())
- part := getPart(q, partContent)
- zn, err := parseZettel.Run(r.Context(), zid, q.Get(api.KeySyntax))
- if err != nil {
- a.reportUsecaseError(w, err)
- return
- }
- a.writeEncodedZettelPart(w, zn, parser.ParseMetadata, enc, encStr, part)
- }
-}
-
-func (a *API) writeEncodedZettelPart(
- w http.ResponseWriter, zn *ast.ZettelNode,
- evalMeta encoder.EvalMetaFunc,
- enc api.EncodingEnum, encStr string, part partType,
-) {
- encdr := encoder.Create(enc)
- 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, evalMeta)
- case partMeta:
- _, err = encdr.WriteMeta(&buf, zn.InhMeta, evalMeta)
- case partContent:
- _, err = encdr.WriteContent(&buf, zn)
- }
- if err != nil {
- a.log.Fatal().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
- }
-
- err = writeBuffer(w, &buf, encoding2ContentType(enc))
- a.log.IfErr(err).Zid(zn.Zid).Msg("Write Encoded Zettel")
-}
DELETED web/adapter/api/get_unlinked_refs.go
Index: web/adapter/api/get_unlinked_refs.go
==================================================================
--- web/adapter/api/get_unlinked_refs.go
+++ web/adapter/api/get_unlinked_refs.go
@@ -1,90 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 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.
-//-----------------------------------------------------------------------------
-
-package api
-
-import (
- "bytes"
- "net/http"
- "strings"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/encoder/textenc"
- "zettelstore.de/z/usecase"
- "zettelstore.de/z/web/adapter"
-)
-
-// MakeListUnlinkedMetaHandler creates a new HTTP handler for the use case "list unlinked references".
-func (a *API) MakeListUnlinkedMetaHandler(
- getMeta usecase.GetMeta,
- unlinkedRefs usecase.UnlinkedReferences,
- evaluate *usecase.Evaluate,
-) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- zid, err := id.Parse(r.URL.Path[1:])
- if err != nil {
- http.NotFound(w, r)
- return
- }
- ctx := r.Context()
- zm, err := getMeta.Run(ctx, zid)
- if err != nil {
- a.reportUsecaseError(w, err)
- return
- }
-
- q := r.URL.Query()
- phrase := q.Get(api.QueryKeyPhrase)
- if phrase == "" {
- if zmkTitle, found := zm.Get(api.KeyTitle); found {
- isTitle := evaluate.RunMetadata(ctx, zmkTitle)
- encdr := textenc.Create()
- var b strings.Builder
- _, err = encdr.WriteInlines(&b, &isTitle)
- if err == nil {
- phrase = b.String()
- }
- }
- }
-
- metaList, err := unlinkedRefs.Run(
- ctx, phrase, adapter.AddUnlinkedRefsToSearch(adapter.GetSearch(q), zm))
- if err != nil {
- a.reportUsecaseError(w, err)
- return
- }
-
- result := api.ZidMetaRelatedList{
- ID: api.ZettelID(zid.String()),
- Meta: zm.Map(),
- Rights: a.getRights(ctx, zm),
- List: make([]api.ZidMetaJSON, 0, len(metaList)),
- }
- for _, m := range metaList {
- result.List = append(result.List, api.ZidMetaJSON{
- ID: api.ZettelID(m.Zid.String()),
- Meta: m.Map(),
- Rights: a.getRights(ctx, m),
- })
- }
-
- var buf bytes.Buffer
- err = encodeJSONData(&buf, result)
- if err != nil {
- a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store unlinked references in buffer")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
-
- err = writeBuffer(w, &buf, ctJSON)
- a.log.IfErr(err).Zid(zid).Msg("Write Unlinked References")
- }
-}
Index: web/adapter/api/get_zettel.go
==================================================================
--- web/adapter/api/get_zettel.go
+++ web/adapter/api/get_zettel.go
@@ -1,139 +1,182 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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 provides api handlers for web requests.
package api
import (
"bytes"
"context"
+ "fmt"
"net/http"
- "zettelstore.de/c/api"
- "zettelstore.de/z/box"
- "zettelstore.de/z/domain"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/usecase"
-)
-
-// MakeGetZettelHandler creates a new HTTP handler to return a zettel.
-func (a *API) MakeGetZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- z, err := a.getZettelFromPath(ctx, w, r, getZettel)
- if err != nil {
- return
- }
- m := z.Meta
-
- var buf bytes.Buffer
- content, encoding := z.Content.Encode()
- err = encodeJSONData(&buf, api.ZettelJSON{
- ID: api.ZettelID(m.Zid.String()),
- Meta: m.Map(),
- Encoding: encoding,
- Content: content,
- Rights: a.getRights(ctx, m),
- })
- if err != nil {
- a.log.Fatal().Err(err).Zid(m.Zid).Msg("Unable to store zettel in buffer")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
-
- err = writeBuffer(w, &buf, ctJSON)
- a.log.IfErr(err).Zid(m.Zid).Msg("Write JSON Zettel")
- }
-}
-
-// MakeGetPlainZettelHandler creates a new HTTP handler to return a zettel in plain formar
-func (a *API) MakeGetPlainZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- z, err := a.getZettelFromPath(box.NoEnrichContext(r.Context()), w, r, getZettel)
- if err != nil {
- return
- }
-
- var buf bytes.Buffer
- var contentType string
- switch getPart(r.URL.Query(), partContent) {
- 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 = ctPlainText
- _, err = z.Meta.Write(&buf)
- case partContent:
- if ct, ok := syntax2contentType(z.Meta.GetDefault(api.KeySyntax, "")); ok {
- contentType = ct
- }
- _, err = z.Content.Write(&buf)
- }
- if err != nil {
- a.log.Fatal().Err(err).Zid(z.Meta.Zid).Msg("Unable to store plain zettel/part in buffer")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
- err = writeBuffer(w, &buf, contentType)
- a.log.IfErr(err).Zid(z.Meta.Zid).Msg("Write Plain Zettel")
- }
-}
-
-func (a *API) getZettelFromPath(ctx context.Context, w http.ResponseWriter, r *http.Request, getZettel usecase.GetZettel) (domain.Zettel, error) {
- zid, err := id.Parse(r.URL.Path[1:])
- if err != nil {
- http.NotFound(w, r)
- return domain.Zettel{}, err
- }
-
- z, err := getZettel.Run(ctx, zid)
- if err != nil {
- a.reportUsecaseError(w, err)
- return domain.Zettel{}, err
- }
- return z, nil
-}
-
-// MakeGetMetaHandler creates a new HTTP handler to return metadata of a zettel.
-func (a *API) MakeGetMetaHandler(getMeta usecase.GetMeta) http.HandlerFunc {
+ "t73f.de/r/sx"
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/sexp"
+ "zettelstore.de/z/ast"
+ "zettelstore.de/z/box"
+ "zettelstore.de/z/encoder"
+ "zettelstore.de/z/parser"
+ "zettelstore.de/z/usecase"
+ "zettelstore.de/z/web/adapter"
+ "zettelstore.de/z/web/content"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
+
+// 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.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
zid, err := id.Parse(r.URL.Path[1:])
if err != nil {
http.NotFound(w, r)
return
}
- ctx := r.Context()
- m, err := getMeta.Run(ctx, zid)
- if err != nil {
- a.reportUsecaseError(w, err)
- return
- }
-
- var buf bytes.Buffer
- err = encodeJSONData(&buf, api.MetaJSON{
- Meta: m.Map(),
- Rights: a.getRights(ctx, m),
- })
- if err != nil {
- a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store metadata in buffer")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
-
- err = writeBuffer(w, &buf, ctJSON)
- a.log.IfErr(err).Zid(zid).Msg("Write JSON Meta")
+ q := r.URL.Query()
+ part := getPart(q, partContent)
+ ctx := r.Context()
+ switch enc, encStr := getEncoding(r, q); enc {
+ case api.EncoderPlain:
+ a.writePlainData(w, ctx, zid, part, getZettel)
+
+ case api.EncoderData:
+ a.writeSzData(w, ctx, zid, part, getZettel)
+
+ default:
+ var zn *ast.ZettelNode
+ var em func(value string) ast.InlineSlice
+ if q.Has(api.QueryKeyParseOnly) {
+ zn, err = parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
+ em = parser.ParseMetadata
+ } else {
+ zn, err = evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
+ em = func(value string) ast.InlineSlice {
+ return evaluate.RunMetadata(ctx, value)
+ }
+ }
+ if err != nil {
+ a.reportUsecaseError(w, err)
+ return
+ }
+ a.writeEncodedZettelPart(ctx, w, zn, em, enc, encStr, part)
+ }
+ }
+}
+
+func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, 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(z.Meta.GetDefault(api.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(w http.ResponseWriter, ctx context.Context, 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,
+ evalMeta encoder.EvalMetaFunc,
+ enc api.EncodingEnum, encStr string, part partType,
+) {
+ encdr := encoder.Create(
+ enc,
+ &encoder.CreateParameter{
+ Lang: a.rtConfig.Get(ctx, zn.InhMeta, api.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, evalMeta)
+ case partMeta:
+ _, err = encdr.WriteMeta(&buf, zn.InhMeta, evalMeta)
+ case partContent:
+ _, err = encdr.WriteContent(&buf, zn)
+ }
+ 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")
}
}
DELETED web/adapter/api/get_zettel_context.go
Index: web/adapter/api/get_zettel_context.go
==================================================================
--- web/adapter/api/get_zettel_context.go
+++ web/adapter/api/get_zettel_context.go
@@ -1,51 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 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.
-//-----------------------------------------------------------------------------
-
-// Package api provides api handlers for web requests.
-package api
-
-import (
- "net/http"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/usecase"
- "zettelstore.de/z/web/adapter"
-)
-
-// MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context".
-func (a *API) MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc {
- return 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()
- dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir))
- depth, ok := adapter.GetInteger(q, api.QueryKeyDepth)
- if !ok || depth < 0 {
- depth = 5
- }
- limit, ok := adapter.GetInteger(q, api.QueryKeyLimit)
- if !ok || limit < 0 {
- limit = 200
- }
- ctx := r.Context()
- metaList, err := getContext.Run(ctx, zid, dir, depth, limit)
- if err != nil {
- a.reportUsecaseError(w, err)
- return
- }
-
- err = a.writeMetaList(ctx, w, metaList[0], metaList[1:])
- a.log.IfErr(err).Zid(zid).Msg("Write Context")
- }
-}
DELETED web/adapter/api/get_zettel_list.go
Index: web/adapter/api/get_zettel_list.go
==================================================================
--- web/adapter/api/get_zettel_list.go
+++ web/adapter/api/get_zettel_list.go
@@ -1,87 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 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.
-//-----------------------------------------------------------------------------
-
-// Package api provides api handlers for web requests.
-package api
-
-import (
- "bytes"
- "fmt"
- "net/http"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/usecase"
- "zettelstore.de/z/web/adapter"
-)
-
-// MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel".
-func (a *API) MakeListMetaHandler(listMeta usecase.ListMeta) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- q := r.URL.Query()
- s := adapter.GetSearch(q)
- metaList, err := listMeta.Run(ctx, s)
- if err != nil {
- a.reportUsecaseError(w, err)
- return
- }
-
- result := make([]api.ZidMetaJSON, 0, len(metaList))
- for _, m := range metaList {
- result = append(result, api.ZidMetaJSON{
- ID: api.ZettelID(m.Zid.String()),
- Meta: m.Map(),
- Rights: a.getRights(ctx, m),
- })
- }
-
- var buf bytes.Buffer
- err = encodeJSONData(&buf, api.ZettelListJSON{
- Query: s.String(),
- Human: s.Human(),
- List: result,
- })
- if err != nil {
- a.log.Fatal().Err(err).Msg("Unable to store meta list in buffer")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
-
- err = writeBuffer(w, &buf, ctJSON)
- a.log.IfErr(err).Msg("Write JSON List")
- }
-}
-
-// MakeListPlainHandler creates a new HTTP handler for the use case "list some zettel".
-func (a *API) MakeListPlainHandler(listMeta usecase.ListMeta) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- q := r.URL.Query()
- s := adapter.GetSearch(q)
- metaList, err := listMeta.Run(ctx, s)
- if err != nil {
- a.reportUsecaseError(w, err)
- return
- }
-
- var buf bytes.Buffer
- for _, m := range metaList {
- _, err = fmt.Fprintln(&buf, m.Zid.String(), m.GetTitle())
- if err != nil {
- a.log.Fatal().Err(err).Msg("Unable to store plain list in buffer")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
- }
-
- err = writeBuffer(w, &buf, ctPlainText)
- a.log.IfErr(err).Msg("Write Plain List")
- }
-}
DELETED web/adapter/api/json.go
Index: web/adapter/api/json.go
==================================================================
--- web/adapter/api/json.go
+++ web/adapter/api/json.go
@@ -1,73 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 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.
-//-----------------------------------------------------------------------------
-
-// Package api provides api handlers for web requests.
-package api
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "io"
- "net/http"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/domain"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
-)
-
-func encodeJSONData(w io.Writer, data interface{}) error {
- enc := json.NewEncoder(w)
- enc.SetEscapeHTML(false)
- return enc.Encode(data)
-}
-
-func (a *API) writeMetaList(ctx context.Context, w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error {
- outList := make([]api.ZidMetaJSON, len(metaList))
- for i, m := range metaList {
- outList[i].ID = api.ZettelID(m.Zid.String())
- outList[i].Meta = m.Map()
- outList[i].Rights = a.getRights(ctx, m)
- }
-
- var buf bytes.Buffer
- err := encodeJSONData(&buf, api.ZidMetaRelatedList{
- ID: api.ZettelID(m.Zid.String()),
- Meta: m.Map(),
- Rights: a.getRights(ctx, m),
- List: outList,
- })
- if err != nil {
- a.log.Fatal().Err(err).Zid(m.Zid).Msg("Unable to store meta list in buffer")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return nil
- }
-
- return writeBuffer(w, &buf, ctJSON)
-}
-
-func buildZettelFromJSONData(r *http.Request, zid id.Zid) (domain.Zettel, error) {
- var zettel domain.Zettel
- dec := json.NewDecoder(r.Body)
- var zettelData api.ZettelDataJSON
- if err := dec.Decode(&zettelData); err != nil {
- return zettel, err
- }
- m := meta.New(zid)
- for k, v := range zettelData.Meta {
- m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v))
- }
- zettel.Meta = m
- if err := zettel.Content.SetDecoded(zettelData.Content, zettelData.Encoding); err != nil {
- return zettel, err
- }
- return zettel, nil
-}
Index: web/adapter/api/login.go
==================================================================
--- web/adapter/api/login.go
+++ web/adapter/api/login.go
@@ -1,41 +1,44 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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"
- "encoding/json"
"net/http"
"time"
- "zettelstore.de/c/api"
+ "t73f.de/r/sx"
"zettelstore.de/z/auth"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
+ "zettelstore.de/z/zettel/id"
)
// MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API.
func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !a.withAuth() {
- err := a.writeJSONToken(w, "freeaccess", 24*366*10*time.Hour)
- a.log.IfErr(err).Msg("Login/free")
+ 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.KindJSON)
+ token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindAPI)
if err != nil {
a.reportUsecaseError(w, err)
return
}
}
@@ -43,12 +46,13 @@
w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`)
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}
- err := a.writeJSONToken(w, string(token), a.tokenLifetime)
- a.log.IfErr(err).Msg("Login")
+ 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 {
@@ -58,34 +62,18 @@
return ident, cred
}
return "", ""
}
-func (a *API) writeJSONToken(w http.ResponseWriter, token string, lifetime time.Duration) error {
- var buf bytes.Buffer
- je := json.NewEncoder(&buf)
- err := je.Encode(api.AuthJSON{
- Token: token,
- Type: "Bearer",
- Expires: int(lifetime / time.Second),
- })
- if err != nil {
- a.log.Fatal().Err(err).Msg("Unable to store token in buffer")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return nil
- }
-
- return writeBuffer(w, &buf, ctJSON)
-}
-
// MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user.
func (a *API) MakeRenewAuthHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !a.withAuth() {
- err := a.writeJSONToken(w, "freeaccess", 24*366*10*time.Hour)
- a.log.IfErr(err).Msg("Refresh/free")
+ 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")
@@ -93,20 +81,30 @@
}
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 {
- err := a.writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime)
- a.log.IfErr(err).Msg("Write old token")
+ 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
}
- err = a.writeJSONToken(w, string(token), a.tokenLifetime)
- a.log.IfErr(err).Msg("Write renewed token")
+ 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 web/adapter/api/query.go
Index: web/adapter/api/query.go
==================================================================
--- web/adapter/api/query.go
+++ web/adapter/api/query.go
@@ -0,0 +1,301 @@
+//-----------------------------------------------------------------------------
+// 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"
+
+ "t73f.de/r/sx"
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/sexp"
+ "zettelstore.de/z/query"
+ "zettelstore.de/z/usecase"
+ "zettelstore.de/z/web/adapter"
+ "zettelstore.de/z/web/content"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
+
+// 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.HandlerFunc {
+ return 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 {
+ for _, act := range actions {
+ if act == api.RedirectAction {
+ zid := metaSeq[0].Zid
+ ub := a.NewURLBuilder('z').SetZid(zid.ZettelID())
+ 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 {
+ min, max := -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 {
+ min = num
+ continue
+ }
+ }
+ if strings.HasPrefix(act, api.MaxAction) {
+ if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
+ max = 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, min, max)
+ }
+ }
+ }
+ 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, min, max int) error {
+ arr0 := meta.CreateArrangement(ml, key)
+ arr := make(meta.Arrangement, len(arr0))
+ for k0, ml0 := range arr0 {
+ if len(ml0) < min || (max > 0 && len(ml0) > max) {
+ 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, 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 {
+ result := make(sx.Vector, len(ml)+1)
+ result[0] = sx.SymbolList
+ symID, symZettel := sx.MakeSymbol("id"), sx.MakeSymbol("zettel")
+ for i, 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)
+ result[i+1] = msz
+ }
+
+ _, err := sx.Print(w, sx.MakeList(
+ sx.MakeSymbol("meta-list"),
+ sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())),
+ sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())),
+ sx.MakeList(result...),
+ ))
+ return err
+}
+func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error {
+ result := sx.Nil()
+ for aggKey, metaList := range arr {
+ sxMeta := sx.Nil()
+ for i := len(metaList) - 1; i >= 0; i-- {
+ sxMeta = sxMeta.Cons(sx.Int64(metaList[i].Zid))
+ }
+ sxMeta = sxMeta.Cons(sx.MakeString(aggKey))
+ result = result.Cons(sxMeta)
+ }
+ _, err := sx.Print(w, sx.MakeList(
+ sx.MakeSymbol("aggregate"),
+ sx.MakeString(act),
+ sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())),
+ sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())),
+ result.Cons(sx.SymbolList),
+ ))
+ 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, tag)
+ if err != nil {
+ a.reportUsecaseError(w, err)
+ return true
+ }
+ zid := z.Meta.Zid
+ newURL := a.NewURLBuilder('z').SetZid(zid.ZettelID())
+ 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, role)
+ if err != nil {
+ a.reportUsecaseError(w, err)
+ return true
+ }
+ zid := z.Meta.Zid
+ newURL := a.NewURLBuilder('z').SetZid(zid.ZettelID())
+ 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")
+ }
+}
Index: web/adapter/api/rename_zettel.go
==================================================================
--- web/adapter/api/rename_zettel.go
+++ web/adapter/api/rename_zettel.go
@@ -1,24 +1,27 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021 Detlef Stern
+// Copyright (c) 2021-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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"
"net/url"
- "zettelstore.de/c/api"
- "zettelstore.de/z/domain/id"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/usecase"
+ "zettelstore.de/z/zettel/id"
)
// MakeRenameZettelHandler creates a new HTTP handler to update a zettel.
func (a *API) MakeRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
Index: web/adapter/api/request.go
==================================================================
--- web/adapter/api/request.go
+++ web/adapter/api/request.go
@@ -1,43 +1,47 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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 provides api handlers for web requests.
package api
import (
"io"
"net/http"
"net/url"
- "zettelstore.de/c/api"
- "zettelstore.de/z/domain"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
- "zettelstore.de/z/input"
+ "t73f.de/r/sx/sxreader"
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/input"
+ "t73f.de/r/zsc/sexp"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
)
// getEncoding returns the data encoding selected by the caller.
-func getEncoding(r *http.Request, q url.Values, defEncoding api.EncodingEnum) (api.EncodingEnum, string) {
+func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) {
encoding := q.Get(api.QueryKeyEncoding)
- if len(encoding) > 0 {
+ 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 defEncoding, defEncoding.String()
+ 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 {
@@ -48,12 +52,11 @@
}
return "", false
}
var mapCT2encoding = map[string]string{
- "application/json": "json",
- "text/html": api.EncodingHTML,
+ "text/html": api.EncodingHTML,
}
func contentType2encoding(contentType string) (string, bool) {
// TODO: only check before first ';'
enc, ok := mapCT2encoding[contentType]
@@ -99,18 +102,48 @@
return ""
}
return p.String()
}
-func buildZettelFromPlainData(r *http.Request, zid id.Zid) (domain.Zettel, error) {
+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 domain.Zettel{}, err
+ return zettel.Zettel{}, err
}
inp := input.NewInput(b)
m := meta.NewFromInput(zid, inp)
- return domain.Zettel{
+ return zettel.Zettel{
Meta: m,
- Content: domain.NewContent(inp.Src[inp.Pos:]),
+ 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.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 web/adapter/api/response.go
Index: web/adapter/api/response.go
==================================================================
--- web/adapter/api/response.go
+++ web/adapter/api/response.go
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+// 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"
+ "zettelstore.de/z/web/content"
+ "zettelstore.de/z/zettel/id"
+)
+
+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)
+}
Index: web/adapter/api/update_zettel.go
==================================================================
--- web/adapter/api/update_zettel.go
+++ web/adapter/api/update_zettel.go
@@ -1,55 +1,51 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021 Detlef Stern
+// Copyright (c) 2021-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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"
- "zettelstore.de/z/domain/id"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
-)
-
-// MakeUpdatePlainZettelHandler creates a new HTTP handler to update a zettel.
-func (a *API) MakeUpdatePlainZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- zid, err := id.Parse(r.URL.Path[1:])
- if err != nil {
- http.NotFound(w, r)
- return
- }
- zettel, err := buildZettelFromPlainData(r, zid)
- 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)
- }
-}
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+)
// MakeUpdateZettelHandler creates a new HTTP handler to update a zettel.
func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
zid, err := id.Parse(r.URL.Path[1:])
if err != nil {
http.NotFound(w, r)
return
}
- zettel, err := buildZettelFromJSONData(r, zid)
+
+ 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 {
Index: web/adapter/errors.go
==================================================================
--- web/adapter/errors.go
+++ web/adapter/errors.go
@@ -1,29 +1,26 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2021 Detlef Stern
+// Copyright (c) 2020-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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 provides handlers for web requests.
package adapter
import "net/http"
// BadRequest signals HTTP status code 400.
func BadRequest(w http.ResponseWriter, text string) {
http.Error(w, text, http.StatusBadRequest)
}
-// Forbidden signals HTTP status code 403.
-func Forbidden(w http.ResponseWriter, text string) {
- http.Error(w, text, http.StatusForbidden)
-}
-
-// NotFound signals HTTP status code 404.
-func NotFound(w http.ResponseWriter, text string) {
- http.Error(w, text, http.StatusNotFound)
-}
+// 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 }
Index: web/adapter/request.go
==================================================================
--- web/adapter/request.go
+++ web/adapter/request.go
@@ -1,13 +1,16 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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 (
@@ -14,15 +17,13 @@
"net/http"
"net/url"
"strconv"
"strings"
- "zettelstore.de/c/api"
- "zettelstore.de/z/domain/meta"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/kernel"
- "zettelstore.de/z/search"
- "zettelstore.de/z/usecase"
+ "zettelstore.de/z/query"
)
// GetCredentialsViaForm retrieves the authentication credentions from a form.
func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) {
err := r.ParseForm()
@@ -37,108 +38,20 @@
return "", "", false
}
return ident, cred, true
}
-// GetInteger returns the integer value of the named query key.
-func GetInteger(q url.Values, key string) (int, bool) {
- s := q.Get(key)
- if s != "" {
- if val, err := strconv.Atoi(s); err == nil {
- return val, true
- }
- }
- return 0, false
-}
-
-// GetSearch retrieves the specified search and sorting options from a query.
-func GetSearch(q url.Values) (s *search.Search) {
- if exprs, found := q[api.QueryKeySearch]; found {
- s = search.Parse(strings.Join(exprs, " "))
- }
- for key, values := range q {
- switch key {
- case api.QueryKeySort, api.QueryKeyOrder:
- s = extractOrderFromQuery(values, s)
- case api.QueryKeyOffset:
- s = extractOffsetFromQuery(values, s)
- case api.QueryKeyLimit:
- s = extractLimitFromQuery(values, s)
- case api.QueryKeyNegate:
- s = s.SetNegate()
- case api.QueryKeySearch: // Ignore, already processed to top of method.
- default:
- if meta.KeyIsValid(key) {
- s = setCleanedQueryValues(s, key, values)
- }
- }
- }
- return s
-}
-
-func extractOrderFromQuery(values []string, s *search.Search) *search.Search {
- if len(values) > 0 {
- descending := false
- sortkey := values[0]
- if strings.HasPrefix(sortkey, "-") {
- descending = true
- sortkey = sortkey[1:]
- }
- if meta.KeyIsValid(sortkey) || sortkey == search.RandomOrder {
- s = s.AddOrder(sortkey, descending)
- }
- }
- return s
-}
-
-func extractOffsetFromQuery(values []string, s *search.Search) *search.Search {
- if len(values) > 0 {
- if offset, err := strconv.Atoi(values[0]); err == nil && offset > 0 {
- s = s.SetOffset(offset)
- }
- }
- return s
-}
-
-func extractLimitFromQuery(values []string, s *search.Search) *search.Search {
- if len(values) > 0 {
- if limit, err := strconv.Atoi(values[0]); err == nil && limit > 0 {
- s = s.SetLimit(limit)
- }
- }
- return s
-}
-
-func setCleanedQueryValues(s *search.Search, key string, values []string) *search.Search {
- for _, val := range values {
- s = s.AddExpr(key, val)
- }
- return s
-}
-
-// GetZCDirection returns a direction value for a given string.
-func GetZCDirection(s string) usecase.ZettelContextDirection {
- switch s {
- case api.DirBackward:
- return usecase.ZettelContextBackward
- case api.DirForward:
- return usecase.ZettelContextForward
- }
- return usecase.ZettelContextBoth
-}
-
-// AddUnlinkedRefsToSearch inspects metadata and enhances the given search to ignore
-// some zettel identifier.
-func AddUnlinkedRefsToSearch(s *search.Search, m *meta.Meta) *search.Search {
- s = s.AddExpr(api.KeyID, "!="+m.Zid.String())
- for _, pair := range m.ComputedPairsRest() {
- switch meta.Type(pair.Key) {
- case meta.TypeID:
- s = s.AddExpr(api.KeyID, "!="+pair.Value)
- case meta.TypeIDSet:
- for _, value := range meta.ListFromValue(pair.Value) {
- s = s.AddExpr(api.KeyID, "!="+value)
- }
- }
- }
- return s
+// 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
}
Index: web/adapter/response.go
==================================================================
--- web/adapter/response.go
+++ web/adapter/response.go
@@ -1,27 +1,42 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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 provides handlers for web requests.
package adapter
import (
"errors"
"fmt"
"net/http"
+ "strings"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/box"
"zettelstore.de/z/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 != "" {
@@ -34,30 +49,44 @@
type ErrBadRequest struct {
Text string
}
// NewErrBadRequest creates an new bad request error.
-func NewErrBadRequest(text string) error { return &ErrBadRequest{Text: text} }
+func NewErrBadRequest(text string) error { return ErrBadRequest{Text: text} }
-func (err *ErrBadRequest) Error() string { return err.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) {
- if err == box.ErrNotFound {
- return http.StatusNotFound, http.StatusText(http.StatusNotFound)
- }
- if err1, ok := err.(*box.ErrNotAllowed); ok {
- return http.StatusForbidden, err1.Error()
- }
- if err1, ok := err.(*box.ErrInvalidID); ok {
- return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", err1.Zid)
- }
- if err1, ok := err.(*usecase.ErrZidInUse); ok {
- return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", err1.Zid)
- }
- if err1, ok := err.(*ErrBadRequest); ok {
- return http.StatusBadRequest, err1.Text
+ 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 ezin usecase.ErrZidInUse
+ if errors.As(err, &ezin) {
+ return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", ezin.Zid)
+ }
+ var etznf usecase.ErrTagZettelNotFound
+ if errors.As(err, &etznf) {
+ return http.StatusNotFound, "Tag zettel not found: " + etznf.Tag
+ }
+ var erznf usecase.ErrRoleZettelNotFound
+ if errors.As(err, &erznf) {
+ return http.StatusNotFound, "Role zettel not found: " + 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) {
@@ -64,7 +93,11 @@
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()
}
Index: web/adapter/webui/const.go
==================================================================
--- web/adapter/webui/const.go
+++ web/adapter/webui/const.go
@@ -1,44 +1,53 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2022 Detlef Stern
+// 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"
+const queryKeyAction = "_action"
// Values for queryKeyAction
const (
- valueActionCopy = "copy"
- valueActionFolge = "folge"
- valueActionNew = "new"
+ valueActionChild = "child"
+ valueActionCopy = "copy"
+ valueActionFolge = "folge"
+ valueActionNew = "new"
+ valueActionVersion = "version"
)
// Enumeration for queryKeyAction
type createAction uint8
const (
- actionCopy createAction = iota
- actionFolge
- actionNew
-)
-
-func getCreateAction(s string) createAction {
- switch s {
- case valueActionCopy:
- return actionCopy
- case valueActionFolge:
- return actionFolge
- case valueActionNew:
- return actionNew
- default:
- return actionCopy
- }
+ actionChild createAction = iota
+ actionCopy
+ actionFolge
+ actionNew
+ actionVersion
+)
+
+var createActionMap = map[string]createAction{
+ valueActionChild: actionChild,
+ valueActionCopy: actionCopy,
+ valueActionFolge: actionFolge,
+ valueActionNew: actionNew,
+ valueActionVersion: actionVersion,
+}
+
+func getCreateAction(s string) createAction {
+ if action, found := createActionMap[s]; found {
+ return action
+ }
+ return actionCopy
}
Index: web/adapter/webui/create_zettel.go
==================================================================
--- web/adapter/webui/create_zettel.go
+++ web/adapter/webui/create_zettel.go
@@ -1,30 +1,38 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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"
- "zettelstore.de/c/api"
+ "t73f.de/r/sx"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/box"
- "zettelstore.de/z/config"
- "zettelstore.de/z/domain"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/encoder/zmkenc"
+ "zettelstore.de/z/evaluator"
"zettelstore.de/z/parser"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
+ "zettelstore.de/z/web/server"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
)
// MakeGetCreateZettelHandler creates a new HTTP handler to display the
// HTML edit view for the various zettel creation methods.
func (wui *WebUI) MakeGetCreateZettelHandler(
@@ -32,41 +40,36 @@
ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
q := r.URL.Query()
op := getCreateAction(q.Get(queryKeyAction))
- zid, err := id.Parse(r.URL.Path[1:])
+ path := r.URL.Path[1:]
+ zid, err := id.Parse(path)
if err != nil {
- wui.reportError(ctx, w, box.ErrNotFound)
+ 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.ErrNotFound)
+ wui.reportError(ctx, w, box.ErrZettelNotFound{Zid: zid})
return
}
roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
switch op {
+ case actionChild:
+ wui.renderZettelForm(ctx, w, createZettel.PrepareChild(origZettel), "Child Zettel", "", roleData, syntaxData)
case actionCopy:
- wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "Copy Zettel", roleData, syntaxData)
+ wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData)
case actionFolge:
- wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "Folgezettel", roleData, syntaxData)
+ wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData)
case actionNew:
- m := origZettel.Meta
- title := parser.ParseMetadata(m.GetTitle())
- textTitle, err2 := encodeInlinesText(&title, wui.gentext)
- if err2 != nil {
- wui.reportError(ctx, w, err2)
- return
- }
- htmlTitle, err2 := wui.getSimpleHTMLEncoder().InlinesString(&title)
- if err2 != nil {
- wui.reportError(ctx, w, err2)
- return
- }
- wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel), textTitle, htmlTitle, roleData, syntaxData)
+ title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle())
+ newTitle := parser.NormalizedSpacedText(q.Get(api.KeyTitle))
+ wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", 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) {
@@ -85,57 +88,104 @@
}
func (wui *WebUI) renderZettelForm(
ctx context.Context,
w http.ResponseWriter,
- zettel domain.Zettel,
- title, heading string,
+ ztl zettel.Zettel,
+ title string,
+ formActionURL string,
roleData []string,
syntaxData []string,
) {
- user := wui.getUser(ctx)
- m := zettel.Meta
- var base baseData
- wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), title, "", user, &base)
- wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{
- Heading: heading,
- MetaTitle: m.GetDefault(api.KeyTitle, ""),
- MetaTags: m.GetDefault(api.KeyTags, ""),
- MetaRole: m.GetDefault(api.KeyRole, ""),
- HasRoleData: len(roleData) > 0,
- RoleData: roleData,
- HasSyntaxData: len(syntaxData) > 0,
- SyntaxData: syntaxData,
- MetaSyntax: m.GetDefault(api.KeySyntax, ""),
- MetaPairsRest: m.PairsRest(),
- IsTextContent: !zettel.Content.IsBinary(),
- Content: zettel.Content.AsString(),
- })
+ user := server.GetUser(ctx)
+ m := ztl.Meta
+
+ var sb strings.Builder
+ for _, p := range m.PairsRest() {
+ sb.WriteString(p.Key)
+ sb.WriteString(": ")
+ sb.WriteString(p.Value)
+ sb.WriteByte('\n')
+ }
+ env, rb := wui.createRenderEnv(ctx, "form", wui.rtConfig.Get(ctx, nil, api.KeyLang), 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.FormTemplateZid, 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.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- reEdit, zettel, hasContent, err := parseZettelForm(r, id.Invalid)
- if err != nil {
- wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read form data"))
+ reEdit, zettel, err := parseZettelForm(r, id.Invalid)
+ if err == errMissingContent {
+ wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing"))
return
}
- if !hasContent {
- wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing"))
+ 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(api.ZettelID(newZid.String())))
+ wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(newZid.ZettelID()))
} else {
- wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String())))
+ wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID()))
+ }
+ }
+}
+
+// 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.HandlerFunc {
+
+ return 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, wui.rtConfig)
+ bns := evaluate.RunBlockNode(ctx, entries)
+ enc := zmkenc.Create()
+ 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(api.KeyTitle, q.Human())
+ m.Set(api.KeySyntax, api.ValueSyntaxZmk)
+ if qval := q.String(); qval != "" {
+ m.Set(api.KeyQuery, 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)
}
}
Index: web/adapter/webui/delete_zettel.go
==================================================================
--- web/adapter/webui/delete_zettel.go
+++ web/adapter/webui/delete_zettel.go
@@ -1,113 +1,78 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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/c/api"
- "zettelstore.de/c/maps"
- "zettelstore.de/z/box"
- "zettelstore.de/z/config"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
- "zettelstore.de/z/strfun"
- "zettelstore.de/z/usecase"
-)
-
-// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
-// HTML delete view of a zettel.
-func (wui *WebUI) MakeGetDeleteZettelHandler(
- getMeta usecase.GetMeta,
- getAllMeta usecase.GetAllMeta,
- evaluate *usecase.Evaluate,
-) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- zid, err := id.Parse(r.URL.Path[1:])
- if err != nil {
- wui.reportError(ctx, w, box.ErrNotFound)
- return
- }
-
- ms, err := getAllMeta.Run(ctx, zid)
- if err != nil {
- wui.reportError(ctx, w, err)
- return
- }
- m := ms[0]
-
- var shadowedBox string
- var incomingLinks []simpleLink
- if len(ms) > 1 {
- shadowedBox = ms[1].GetDefault(api.KeyBoxNumber, "???")
- } else {
- getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate)
- incomingLinks = wui.encodeIncoming(m, getTextTitle)
- }
- uselessFiles := retrieveUselessFiles(m)
-
- user := wui.getUser(ctx)
- var base baseData
- wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Delete Zettel "+m.Zid.String(), "", user, &base)
- wui.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct {
- Zid string
- MetaPairs []meta.Pair
- HasShadows bool
- ShadowedBox string
- HasIncoming bool
- Incoming []simpleLink
- HasUselessFiles bool
- UselessFiles []string
- }{
- Zid: zid.String(),
- MetaPairs: m.ComputedPairs(),
- HasShadows: shadowedBox != "",
- ShadowedBox: shadowedBox,
- HasIncoming: len(incomingLinks) > 0,
- Incoming: incomingLinks,
- HasUselessFiles: len(uselessFiles) > 0,
- UselessFiles: uselessFiles,
- })
- }
-}
-
-func retrieveUselessFiles(m *meta.Meta) []string {
- if val, found := m.Get(api.KeyUselessFiles); found {
- return []string{val}
- }
- return nil
-}
-
-// MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel.
-func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- zid, err := id.Parse(r.URL.Path[1:])
- if err != nil {
- wui.reportError(ctx, w, box.ErrNotFound)
- return
- }
-
- if err = deleteZettel.Run(r.Context(), zid); err != nil {
- wui.reportError(ctx, w, err)
- return
- }
- wui.redirectFound(w, r, wui.NewURLBuilder('/'))
- }
-}
-
-func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) []simpleLink {
+ "t73f.de/r/sx"
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/maps"
+ "zettelstore.de/z/box"
+ "zettelstore.de/z/strfun"
+ "zettelstore.de/z/usecase"
+ "zettelstore.de/z/web/server"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
+
+// 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.HandlerFunc {
+ return 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.rtConfig.Get(ctx, nil, api.KeyLang), "Delete Zettel "+m.Zid.String(), user)
+ if len(zs) > 1 {
+ rb.bindString("shadowed-box", sx.MakeString(zs[1].Meta.GetDefault(api.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.DeleteTemplateZid, 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 := make(strfun.Set)
addListValues(zidMap, m, api.KeyBackward)
for _, kd := range meta.GetSortedKeyDescriptions() {
inverseKey := kd.Inverse
if inverseKey == "" {
@@ -121,15 +86,34 @@
}
case meta.TypeIDSet:
addListValues(zidMap, m, inverseKey)
}
}
- return wui.encodeZidLinks(maps.Keys(zidMap), getTextTitle)
+ return wui.zidLinksSxn(maps.Keys(zidMap), getTextTitle)
}
func addListValues(zidMap strfun.Set, m *meta.Meta, key string) {
if values, ok := m.GetList(key); ok {
for _, val := range values {
zidMap.Set(val)
}
}
}
+
+// MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel.
+func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc {
+ return 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('/'))
+ }
+}
Index: web/adapter/webui/edit_zettel.go
==================================================================
--- web/adapter/webui/edit_zettel.go
+++ web/adapter/webui/edit_zettel.go
@@ -1,35 +1,38 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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/c/api"
"zettelstore.de/z/box"
- "zettelstore.de/z/domain/id"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
+ "zettelstore.de/z/zettel/id"
)
// 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.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- zid, err := id.Parse(r.URL.Path[1:])
+ path := r.URL.Path[1:]
+ zid, err := id.Parse(path)
if err != nil {
- wui.reportError(ctx, w, box.ErrNotFound)
+ wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
return
}
zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
if err != nil {
@@ -36,38 +39,44 @@
wui.reportError(ctx, w, err)
return
}
roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
- wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "Edit Zettel", roleData, syntaxData)
+ 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.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- zid, err := id.Parse(r.URL.Path[1:])
+ path := r.URL.Path[1:]
+ zid, err := id.Parse(path)
if err != nil {
- wui.reportError(ctx, w, box.ErrNotFound)
+ wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
return
}
- reEdit, zettel, hasContent, err := parseZettelForm(r, zid)
+ reEdit, zettel, err := parseZettelForm(r, zid)
+ hasContent := true
if err != nil {
- wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read zettel form"))
- return
+ 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(api.ZettelID(zid.String())))
+ wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(zid.ZettelID()))
} else {
- wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())))
+ wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid.ZettelID()))
}
}
}
ADDED web/adapter/webui/favicon.go
Index: web/adapter/webui/favicon.go
==================================================================
--- web/adapter/webui/favicon.go
+++ web/adapter/webui/favicon.go
@@ -0,0 +1,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 webui
+
+import (
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+
+ "zettelstore.de/z/web/adapter"
+)
+
+func (wui *WebUI) MakeFaviconHandler(baseDir string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *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")
+ }
+ }
+}
Index: web/adapter/webui/forms.go
==================================================================
--- web/adapter/webui/forms.go
+++ web/adapter/webui/forms.go
@@ -1,54 +1,51 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/domain"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
- "zettelstore.de/z/input"
-)
-
-type formZettelData struct {
- Heading string
- MetaTitle string
- MetaRole string
- HasRoleData bool
- RoleData []string
- HasSyntaxData bool
- SyntaxData []string
- MetaTags string
- MetaSyntax string
- MetaPairsRest []meta.Pair
- IsTextContent bool
- Content string
-}
+ "unicode"
+
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/input"
+ "zettelstore.de/z/kernel"
+ "zettelstore.de/z/parser"
+ "zettelstore.de/z/web/content"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
var (
bsCRLF = []byte{'\r', '\n'}
bsLF = []byte{'\n'}
)
-func parseZettelForm(r *http.Request, zid id.Zid) (bool, domain.Zettel, bool, error) {
- err := r.ParseForm()
+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, domain.Zettel{}, false, err
+ return false, zettel.Zettel{}, err
}
_, doSave := r.Form["save"]
var m *meta.Meta
if postMeta, ok := trimmedFormValue(r, "meta"); ok {
@@ -59,30 +56,35 @@
}
if postTitle, ok := trimmedFormValue(r, "title"); ok {
m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle))
}
if postTags, ok := trimmedFormValue(r, "tags"); ok {
- if tags := strings.Fields(meta.RemoveNonGraphic(postTags)); len(tags) > 0 {
+ if tags := meta.ListFromValue(meta.RemoveNonGraphic(postTags)); len(tags) > 0 {
+ for i, tag := range tags {
+ tags[i] = meta.NormalizeTag(tag)
+ }
m.SetList(api.KeyTags, tags)
}
}
if postRole, ok := trimmedFormValue(r, "role"); ok {
- m.Set(api.KeyRole, meta.RemoveNonGraphic(postRole))
+ m.SetWord(api.KeyRole, meta.RemoveNonGraphic(postRole))
}
if postSyntax, ok := trimmedFormValue(r, "syntax"); ok {
- m.Set(api.KeySyntax, meta.RemoveNonGraphic(postSyntax))
- }
- if values, ok := r.PostForm["content"]; ok && len(values) > 0 {
- return doSave, domain.Zettel{
- Meta: m,
- Content: domain.NewContent(bytes.ReplaceAll([]byte(values[0]), bsCRLF, bsLF)),
- }, true, nil
- }
- return doSave, domain.Zettel{
- Meta: m,
- Content: domain.NewContent(nil),
- }, false, nil
+ m.SetWord(api.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])
@@ -90,12 +92,56 @@
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(api.KeySyntax, fileSyntax)
+ }
+ }
+ return data, m
+ }
+ }
+ return nil, m
+}
+
+func allowEmptyContent(m *meta.Meta) bool {
+ if syntax, found := m.Get(api.KeySyntax); found {
+ if syntax == api.ValueSyntaxNone {
+ return true
+ }
+ if pinfo := parser.Get(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'})
}
Index: web/adapter/webui/forms_test.go
==================================================================
--- web/adapter/webui/forms_test.go
+++ web/adapter/webui/forms_test.go
@@ -1,13 +1,16 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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"
Index: web/adapter/webui/get_info.go
==================================================================
--- web/adapter/webui/get_info.go
+++ web/adapter/webui/get_info.go
@@ -1,263 +1,231 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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"
"sort"
"strings"
- "zettelstore.de/c/api"
+ "t73f.de/r/sx"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/ast"
"zettelstore.de/z/box"
"zettelstore.de/z/collect"
- "zettelstore.de/z/config"
- "zettelstore.de/z/domain/id"
"zettelstore.de/z/encoder"
+ "zettelstore.de/z/evaluator"
+ "zettelstore.de/z/parser"
+ "zettelstore.de/z/query"
+ "zettelstore.de/z/strfun"
"zettelstore.de/z/usecase"
- "zettelstore.de/z/web/adapter"
-)
-
-type metaDataInfo struct {
- Key string
- Value string
-}
-
-type matrixLine struct {
- Header string
- Elements []simpleLink
-}
+ "zettelstore.de/z/web/server"
+ "zettelstore.de/z/zettel/id"
+)
// MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetInfoHandler(
- parseZettel usecase.ParseZettel,
- evaluate *usecase.Evaluate,
- getMeta usecase.GetMeta,
- getAllMeta usecase.GetAllMeta,
- unlinkedRefs usecase.UnlinkedReferences,
+ ucParseZettel usecase.ParseZettel,
+ ucEvaluate *usecase.Evaluate,
+ ucGetZettel usecase.GetZettel,
+ ucGetAllMeta usecase.GetAllZettel,
+ ucQuery *usecase.Query,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
q := r.URL.Query()
- zid, err := id.Parse(r.URL.Path[1:])
- if err != nil {
- wui.reportError(ctx, w, box.ErrNotFound)
- return
- }
-
- zn, err := parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
- if err != nil {
- wui.reportError(ctx, w, err)
- return
- }
-
- enc := wui.getSimpleHTMLEncoder()
- pairs := zn.Meta.ComputedPairs()
- metaData := make([]metaDataInfo, len(pairs))
- getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate)
- for i, p := range pairs {
- var buf bytes.Buffer
- wui.writeHTMLMetaValue(
- &buf, p.Key, p.Value,
- getTextTitle,
- func(val string) ast.InlineSlice {
- return evaluate.RunMetadata(ctx, val)
- },
- enc)
- metaData[i] = metaDataInfo{p.Key, buf.String()}
- }
- summary := collect.References(zn)
- locLinks, searchQuery, extLinks := splitLocSeaExtLinks(append(summary.Links, summary.Embeds...))
- searchLinks := make([]simpleLink, len(searchQuery))
- for i, sq := range searchQuery {
- searchLinks[i].Text = sq
- searchLinks[i].URL = wui.NewURLBuilder('h').AppendSearch(sq).String()
- }
-
- textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate)
- phrase := q.Get(api.QueryKeyPhrase)
- if phrase == "" {
- phrase = textTitle
- }
- phrase = strings.TrimSpace(phrase)
- unlinkedMeta, err := unlinkedRefs.Run(
- ctx, phrase, adapter.AddUnlinkedRefsToSearch(nil, zn.InhMeta))
- if err != nil {
- wui.reportError(ctx, w, err)
- return
- }
- unLinks := wui.buildHTMLMetaList(unlinkedMeta, func(val string) ast.InlineSlice { return evaluate.RunMetadata(ctx, val) })
-
- shadowLinks := getShadowLinks(ctx, zid, getAllMeta)
- endnotes, err := enc.BlocksString(&ast.BlockSlice{})
- if err != nil {
- endnotes = ""
- }
-
- user := wui.getUser(ctx)
- canCreate := wui.canCreate(ctx, user)
- apiZid := api.ZettelID(zid.String())
- var base baseData
- wui.makeBaseData(ctx, config.GetLang(zn.InhMeta, wui.rtConfig), textTitle, "", user, &base)
- wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct {
- Zid string
- WebURL string
- ContextURL string
- CanWrite bool
- EditURL string
- CanFolge bool
- FolgeURL string
- CanCopy bool
- CopyURL string
- CanRename bool
- RenameURL string
- CanDelete bool
- DeleteURL string
- MetaData []metaDataInfo
- HasLocLinks bool
- LocLinks []localLink
- HasSearchLinks bool
- SearchLinks []simpleLink
- HasExtLinks bool
- ExtLinks []string
- ExtNewWindow string
- UnLinks []simpleLink
- UnLinksPhrase string
- QueryKeyPhrase string
- EvalMatrix []matrixLine
- ParseMatrix []matrixLine
- HasShadowLinks bool
- ShadowLinks []string
- Endnotes string
- }{
- Zid: zid.String(),
- WebURL: wui.NewURLBuilder('h').SetZid(apiZid).String(),
- ContextURL: wui.NewURLBuilder('k').SetZid(apiZid).String(),
- CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content),
- EditURL: wui.NewURLBuilder('e').SetZid(apiZid).String(),
- CanFolge: canCreate,
- FolgeURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionFolge).String(),
- CanCopy: canCreate && !zn.Content.IsBinary(),
- CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionCopy).String(),
- CanRename: wui.canRename(ctx, user, zn.Meta),
- RenameURL: wui.NewURLBuilder('b').SetZid(apiZid).String(),
- CanDelete: wui.canDelete(ctx, user, zn.Meta),
- DeleteURL: wui.NewURLBuilder('d').SetZid(apiZid).String(),
- MetaData: metaData,
- HasLocLinks: len(locLinks) > 0,
- LocLinks: locLinks,
- HasSearchLinks: len(searchQuery) > 0,
- SearchLinks: searchLinks,
- HasExtLinks: len(extLinks) > 0,
- ExtLinks: extLinks,
- ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0),
- UnLinks: unLinks,
- UnLinksPhrase: phrase,
- QueryKeyPhrase: api.QueryKeyPhrase,
- EvalMatrix: wui.infoAPIMatrix('v', zid),
- ParseMatrix: wui.infoAPIMatrixPlain('p', zid),
- HasShadowLinks: len(shadowLinks) > 0,
- ShadowLinks: shadowLinks,
- Endnotes: endnotes,
- })
- }
-}
-
-type localLink struct {
- Valid bool
- Zid string
-}
-
-func splitLocSeaExtLinks(links []*ast.Reference) (locLinks []localLink, searchQuery, extLinks []string) {
- if len(links) == 0 {
- return nil, nil, nil
- }
- for _, ref := range links {
- if ref.State == ast.RefStateSelf || ref.IsZettel() {
- continue
- }
- if ref.State == ast.RefStateSearch {
- searchQuery = append(searchQuery, ref.Value)
- continue
- }
- if ref.IsExternal() {
- extLinks = append(extLinks, ref.String())
- continue
- }
- locLinks = append(locLinks, localLink{ref.IsValid(), ref.String()})
- }
- return locLinks, searchQuery, extLinks
-}
-
-func (wui *WebUI) infoAPIMatrix(key byte, zid id.Zid) []matrixLine {
+ 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(api.KeySyntax))
+ if err != nil {
+ wui.reportError(ctx, w, err)
+ return
+ }
+
+ enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang))
+ getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel)
+ evalMeta := func(val string) ast.InlineSlice {
+ return ucEvaluate.RunMetadata(ctx, val)
+ }
+ pairs := zn.Meta.ComputedPairs()
+ metadata := sx.Nil()
+ for i := len(pairs) - 1; i >= 0; i-- {
+ key := pairs[i].Key
+ sxval := wui.writeHTMLMetaValue(key, pairs[i].Value, getTextTitle, evalMeta, enc)
+ metadata = metadata.Cons(sx.Cons(sx.MakeString(key), sxval))
+ }
+
+ summary := collect.References(zn)
+ locLinks, queryLinks, extLinks := wui.splitLocSeaExtLinks(append(summary.Links, summary.Embeds...))
+
+ title := parser.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
+ }
+
+ entries, _ := evaluator.QueryAction(ctx, nil, unlinkedMeta, wui.rtConfig)
+ 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, ucGetAllMeta)
+
+ user := server.GetUser(ctx)
+ env, rb := wui.createRenderEnv(ctx, "info", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user)
+ rb.bindString("metadata", metadata)
+ 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.InfoTemplateZid, 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) {
+ for i := len(links) - 1; i >= 0; i-- {
+ ref := links[i]
+ if ref.State == ast.RefStateSelf || ref.IsZettel() {
+ continue
+ }
+ if ref.State == ast.RefStateQuery {
+ queries = queries.Cons(
+ sx.Cons(
+ sx.MakeString(ref.Value),
+ sx.MakeString(wui.NewURLBuilder('h').AppendQuery(ref.Value).String())))
+ continue
+ }
+ if ref.IsExternal() {
+ extLinks = extLinks.Cons(sx.MakeString(ref.String()))
+ continue
+ }
+ locLinks = locLinks.Cons(sx.Cons(sx.MakeBoolean(ref.IsValid()), sx.MakeString(ref.String())))
+ }
+ return locLinks, queries, extLinks
+}
+
+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(api.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())
}
sort.Strings(encTexts)
- defEncoding := encoder.GetDefaultEncoding().String()
- parts := getParts()
- matrix := make([]matrixLine, 0, len(parts))
- u := wui.NewURLBuilder(key).SetZid(api.ZettelID(zid.String()))
- for _, part := range parts {
- row := make([]simpleLink, len(encTexts))
- for j, enc := range encTexts {
- u.AppendQuery(api.QueryKeyPart, part)
- if enc != defEncoding {
- u.AppendQuery(api.QueryKeyEncoding, enc)
- }
- row[j] = simpleLink{enc, u.String()}
- u.ClearQuery()
- }
- matrix = append(matrix, matrixLine{part, row})
- }
- return matrix
-}
-
-func (wui *WebUI) infoAPIMatrixPlain(key byte, zid id.Zid) []matrixLine {
- matrix := wui.infoAPIMatrix(key, zid)
- apiZid := api.ZettelID(zid.String())
-
- // Append plain and JSON format
- u := wui.NewURLBuilder('z').SetZid(apiZid)
- for i, part := range getParts() {
- u.AppendQuery(api.QueryKeyPart, part)
- matrix[i].Elements = append(matrix[i].Elements, simpleLink{"plain", u.String()})
- u.ClearQuery()
- }
- u = wui.NewURLBuilder('j').SetZid(apiZid)
- matrix[0].Elements = append(matrix[0].Elements, simpleLink{"json", u.String()})
- u = wui.NewURLBuilder('m').SetZid(apiZid)
- matrix[1].Elements = append(matrix[1].Elements, simpleLink{"json", u.String()})
- return matrix
-}
-
-func getParts() []string {
- return []string{api.PartZettel, api.PartMeta, api.PartContent}
-}
-
-func getShadowLinks(ctx context.Context, zid id.Zid, getAllMeta usecase.GetAllMeta) []string {
- ml, err := getAllMeta.Run(ctx, zid)
- if err != nil || len(ml) < 2 {
- return nil
- }
- result := make([]string, 0, len(ml)-1)
- for _, m := range ml[1:] {
- if boxNo, ok := m.Get(api.KeyBoxNumber); ok {
- result = append(result, boxNo)
+ 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.ZettelID())
+ 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.ZettelID())
+
+ 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, getAllZettel usecase.GetAllZettel) *sx.Pair {
+ result := sx.Nil()
+ if zl, err := getAllZettel.Run(ctx, zid); err == nil {
+ for i := len(zl) - 1; i >= 1; i-- {
+ if boxNo, ok := zl[i].Meta.Get(api.KeyBoxNumber); ok {
+ result = result.Cons(sx.MakeString(boxNo))
+ }
}
}
return result
}
Index: web/adapter/webui/get_zettel.go
==================================================================
--- web/adapter/webui/get_zettel.go
+++ web/adapter/webui/get_zettel.go
@@ -1,190 +1,163 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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"
- "zettelstore.de/c/api"
- "zettelstore.de/z/ast"
+ "t73f.de/r/sx"
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/shtml"
"zettelstore.de/z/box"
"zettelstore.de/z/config"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
- "zettelstore.de/z/encoder/textenc"
+ "zettelstore.de/z/parser"
"zettelstore.de/z/usecase"
+ "zettelstore.de/z/web/server"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
)
// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel".
-func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getMeta usecase.GetMeta) http.HandlerFunc {
+func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getZettel usecase.GetZettel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- zid, err := id.Parse(r.URL.Path[1:])
+ path := r.URL.Path[1:]
+ zid, err := id.Parse(path)
if err != nil {
- wui.reportError(ctx, w, box.ErrNotFound)
+ wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
return
}
q := r.URL.Query()
zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
-
- if err != nil {
- wui.reportError(ctx, w, err)
- return
- }
-
- enc := wui.createZettelEncoder()
- evalMetadata := func(value string) ast.InlineSlice {
- return evaluate.RunMetadata(ctx, value)
- }
- metaHeader := enc.MetaString(zn.InhMeta, evalMetadata)
- textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate)
- htmlTitle := encodeZmkMetadata(zn.InhMeta.GetTitle(), evalMetadata, enc)
- htmlContent, err := enc.BlocksString(&zn.Ast)
- if err != nil {
- wui.reportError(ctx, w, err)
- return
- }
- var roleCSSURL string
- cssZid, err := wui.retrieveCSSZidFromRole(ctx, *zn.InhMeta)
- if err != nil {
- wui.reportError(ctx, w, err)
- return
- }
- if cssZid != id.Invalid {
- roleCSSURL = wui.NewURLBuilder('z').SetZid(api.ZettelID(cssZid.String())).String()
- }
- user := wui.getUser(ctx)
- roleText := zn.Meta.GetDefault(api.KeyRole, "*")
- tags := wui.buildTagInfos(zn.Meta)
- canCreate := wui.canCreate(ctx, user)
- getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate)
- extURL, hasExtURL := zn.Meta.Get(api.KeyURL)
- folgeLinks := wui.encodeZettelLinks(zn.InhMeta, api.KeyFolge, getTextTitle)
- backLinks := wui.encodeZettelLinks(zn.InhMeta, api.KeyBack, getTextTitle)
- apiZid := api.ZettelID(zid.String())
- var base baseData
- wui.makeBaseData(ctx, config.GetLang(zn.InhMeta, wui.rtConfig), textTitle, roleCSSURL, user, &base)
- base.MetaHeader = metaHeader
- wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct {
- HTMLTitle string
- RoleCSS string
- CanWrite bool
- EditURL string
- Zid string
- InfoURL string
- RoleText string
- RoleURL string
- HasTags bool
- Tags []simpleLink
- CanCopy bool
- CopyURL string
- CanFolge bool
- FolgeURL string
- PrecursorRefs string
- HasExtURL bool
- ExtURL string
- ExtNewWindow string
- Content string
- HasFolgeLinks bool
- FolgeLinks []simpleLink
- HasBackLinks bool
- BackLinks []simpleLink
- }{
- HTMLTitle: htmlTitle,
- RoleCSS: roleCSSURL,
- CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content),
- EditURL: wui.NewURLBuilder('e').SetZid(apiZid).String(),
- Zid: zid.String(),
- InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(),
- RoleText: roleText,
- RoleURL: wui.NewURLBuilder('h').AppendQuery("role", roleText).String(),
- HasTags: len(tags) > 0,
- Tags: tags,
- CanCopy: canCreate && !zn.Content.IsBinary(),
- CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionCopy).String(),
- CanFolge: canCreate,
- FolgeURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionFolge).String(),
- PrecursorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPrecursor, getTextTitle),
- ExtURL: extURL,
- HasExtURL: hasExtURL,
- ExtNewWindow: htmlAttrNewWindow(hasExtURL),
- Content: htmlContent,
- HasFolgeLinks: len(folgeLinks) > 0,
- FolgeLinks: folgeLinks,
- HasBackLinks: len(backLinks) > 0,
- BackLinks: backLinks,
- })
- }
-}
-
-func encodeInlinesText(is *ast.InlineSlice, enc *textenc.Encoder) (string, error) {
- if is == nil || len(*is) == 0 {
- return "", nil
- }
-
- var buf bytes.Buffer
- _, err := enc.WriteInlines(&buf, is)
- if err != nil {
- return "", err
- }
- return buf.String(), nil
-}
-
-func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink {
- var tagInfos []simpleLink
- if tags, ok := m.GetList(api.KeyTags); ok {
- ub := wui.NewURLBuilder('h')
- tagInfos = make([]simpleLink, len(tags))
- for i, tag := range tags {
- tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery(api.KeyAllTags, tag).String()}
- ub.ClearQuery()
- }
- }
- return tagInfos
-}
-
-func (wui *WebUI) encodeIdentifierSet(m *meta.Meta, key string, getTextTitle getTextTitleFunc) string {
- if value, ok := m.Get(key); ok {
- var buf bytes.Buffer
- wui.writeIdentifierSet(&buf, meta.ListFromValue(value), getTextTitle)
- return buf.String()
- }
- return ""
-}
-
-func (wui *WebUI) encodeZettelLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) []simpleLink {
- values, ok := m.GetList(key)
- if !ok || len(values) == 0 {
- return nil
- }
- return wui.encodeZidLinks(values, getTextTitle)
-}
-
-func (wui *WebUI) encodeZidLinks(values []string, getTextTitle getTextTitleFunc) []simpleLink {
- result := make([]simpleLink, 0, len(values))
- for _, val := range values {
- zid, err := id.Parse(val)
- if err != nil {
- continue
- }
- if title, found := getTextTitle(zid); found > 0 {
- url := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())).String()
- if title == "" {
- result = append(result, simpleLink{Text: val, URL: url})
- } else {
- result = append(result, simpleLink{Text: title, URL: url})
- }
- }
- }
- return result
+ if err != nil {
+ wui.reportError(ctx, w, err)
+ return
+ }
+
+ enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang))
+ metaObj := enc.MetaSxn(zn.InhMeta, createEvalMetadataFunc(ctx, evaluate))
+ content, endnotes, err := enc.BlocksSxn(&zn.Ast)
+ if err != nil {
+ wui.reportError(ctx, w, err)
+ return
+ }
+
+ user := server.GetUser(ctx)
+ getTextTitle := wui.makeGetTextTitle(ctx, getZettel)
+
+ title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
+ env, rb := wui.createRenderEnv(ctx, "zettel", wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), title, user)
+ rb.bindSymbol(symMetaHeader, metaObj)
+ rb.bindString("heading", sx.MakeString(title))
+ if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
+ rb.bindString("role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String()))
+ }
+ if folgeRole, found := zn.InhMeta.Get(api.KeyFolgeRole); found && folgeRole != "" {
+ rb.bindString("folge-role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+folgeRole).String()))
+ }
+ rb.bindString("tag-refs", wui.transformTagSet(api.KeyTags, meta.ListFromValue(zn.InhMeta.GetDefault(api.KeyTags, ""))))
+ rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPredecessor, getTextTitle))
+ rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrecursor, getTextTitle))
+ rb.bindString("superior-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeySuperior, getTextTitle))
+ rb.bindString("urls", metaURLAssoc(zn.InhMeta))
+ rb.bindString("content", content)
+ rb.bindString("endnotes", endnotes)
+ wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, api.KeyFolge, config.KeyShowFolgeLinks, getTextTitle)
+ wui.bindLinks(ctx, &rb, "subordinate", zn.InhMeta, api.KeySubordinates, config.KeyShowSubordinateLinks, getTextTitle)
+ wui.bindLinks(ctx, &rb, "back", zn.InhMeta, api.KeyBack, config.KeyShowBackLinks, getTextTitle)
+ wui.bindLinks(ctx, &rb, "successor", zn.InhMeta, api.KeySuccessors, config.KeyShowSuccessorLinks, getTextTitle)
+ if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
+ for _, part := range []string{"meta", "actions", "heading"} {
+ rb.rebindResolved("ROLE-"+role+"-"+part, "ROLE-DEFAULT-"+part)
+ }
+ }
+ wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
+ if rb.err == nil {
+ err = wui.renderSxnTemplate(ctx, w, id.ZettelTemplateZid, 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 {
+ if values, ok := m.GetList(key); ok {
+ return wui.transformIdentifierSet(values, getTextTitle)
+ }
+ return nil
+}
+
+func metaURLAssoc(m *meta.Meta) *sx.Pair {
+ var result sx.ListBuilder
+ for _, p := range m.PairsRest() {
+ if key := p.Key; strings.HasSuffix(key, meta.SuffixKeyURL) {
+ if val := p.Value; val != "" {
+ result.Add(sx.Cons(sx.MakeString(capitalizeMetaKey(key)), sx.MakeString(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.rtConfig.Get(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 {
+ values, ok := m.GetList(key)
+ if !ok || len(values) == 0 {
+ return nil
+ }
+ return wui.zidLinksSxn(values, getTextTitle)
+}
+
+func (wui *WebUI) zidLinksSxn(values []string, getTextTitle getTextTitleFunc) (lst *sx.Pair) {
+ for i := len(values) - 1; i >= 0; i-- {
+ val := values[i]
+ zid, err := id.Parse(val)
+ if err != nil {
+ continue
+ }
+ if title, found := getTextTitle(zid); found > 0 {
+ url := sx.MakeString(wui.NewURLBuilder('h').SetZid(zid.ZettelID()).String())
+ if title == "" {
+ lst = lst.Cons(sx.Cons(sx.MakeString(val), url))
+ } else {
+ lst = lst.Cons(sx.Cons(sx.MakeString(title), url))
+ }
+ }
+ }
+ return lst
}
Index: web/adapter/webui/goaction.go
==================================================================
--- web/adapter/webui/goaction.go
+++ web/adapter/webui/goaction.go
@@ -1,13 +1,16 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2021 Detlef Stern
+// Copyright (c) 2020-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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 (
Index: web/adapter/webui/home.go
==================================================================
--- web/adapter/webui/home.go
+++ web/adapter/webui/home.go
@@ -1,57 +1,61 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2021 Detlef Stern
+// Copyright (c) 2020-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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"
- "zettelstore.de/c/api"
"zettelstore.de/z/box"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/config"
+ "zettelstore.de/z/web/adapter"
+ "zettelstore.de/z/web/server"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
)
type getRootStore interface {
- // GetMeta retrieves just the meta data of a specific zettel.
- GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
+ 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 getRootStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- if r.URL.Path != "/" {
- wui.reportError(ctx, w, box.ErrNotFound)
+ if p := r.URL.Path; p != "/" {
+ wui.reportError(ctx, w, adapter.ErrResourceNotFound{Path: p})
return
}
- homeZid := wui.rtConfig.GetHomeZettel()
- apiHomeZid := api.ZettelID(homeZid.String())
+ homeZid, _ := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyHomeZettel))
+ apiHomeZid := homeZid.ZettelID()
if homeZid != id.DefaultHomeZid {
- if _, err := s.GetMeta(ctx, homeZid); err == nil {
+ if _, err := s.GetZettel(ctx, homeZid); err == nil {
wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
return
}
homeZid = id.DefaultHomeZid
}
- _, err := s.GetMeta(ctx, homeZid)
+ _, err := s.GetZettel(ctx, homeZid)
if err == nil {
wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
return
}
- if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && wui.getUser(ctx) == nil {
+ 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'))
}
}
Index: web/adapter/webui/htmlgen.go
==================================================================
--- web/adapter/webui/htmlgen.go
+++ web/adapter/webui/htmlgen.go
@@ -1,217 +1,293 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2022 Detlef Stern
+// 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 (
- "bytes"
+ "net/url"
"strings"
- "codeberg.org/t73fde/sxpf"
- "zettelstore.de/c/api"
- "zettelstore.de/c/html"
- "zettelstore.de/c/sexpr"
+ "t73f.de/r/sx"
+ "t73f.de/r/sxwebs/sxhtml"
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/attrs"
+ "t73f.de/r/zsc/maps"
+ "t73f.de/r/zsc/shtml"
+ "t73f.de/r/zsc/sz"
"zettelstore.de/z/ast"
- "zettelstore.de/z/domain/meta"
"zettelstore.de/z/encoder"
- "zettelstore.de/z/encoder/sexprenc"
- "zettelstore.de/z/encoder/textenc"
- "zettelstore.de/z/search"
+ "zettelstore.de/z/encoder/szenc"
"zettelstore.de/z/strfun"
+ "zettelstore.de/z/zettel/meta"
)
// Builder allows to build new URLs for the web service.
type urlBuilder interface {
GetURLPrefix() string
NewURLBuilder(key byte) *api.URLBuilder
}
type htmlGenerator struct {
- builder urlBuilder
- textEnc *textenc.Encoder
- extMarker string
- newWindow bool
- env *html.EncEnvironment
-}
-
-func createGenerator(builder urlBuilder, extMarker string, newWindow bool) *htmlGenerator {
- env := html.NewEncEnvironment(nil, 1)
- gen := &htmlGenerator{
- builder: builder,
- textEnc: textenc.Create(),
- extMarker: extMarker,
- newWindow: newWindow,
- env: env,
- }
-
- env.Builtins.Set(sexpr.SymTag, sxpf.NewBuiltin("tag", true, 0, -1, gen.generateTag))
- env.Builtins.Set(sexpr.SymLinkZettel, sxpf.NewBuiltin("linkZ", true, 2, -1, gen.generateLinkZettel))
- env.Builtins.Set(sexpr.SymLinkFound, sxpf.NewBuiltin("linkZ", true, 2, -1, gen.generateLinkZettel))
- env.Builtins.Set(sexpr.SymLinkBased, sxpf.NewBuiltin("linkB", true, 2, -1, gen.generateLinkBased))
- env.Builtins.Set(sexpr.SymLinkSearch, sxpf.NewBuiltin("linkS", true, 2, -1, gen.generateLinkSearch))
- env.Builtins.Set(sexpr.SymLinkExternal, sxpf.NewBuiltin("linkE", true, 2, -1, gen.generateLinkExternal))
-
- f, err := env.Builtins.LookupForm(sexpr.SymEmbed)
- if err != nil {
- panic(err)
- }
- b := f.(*sxpf.Builtin)
- env.Builtins.Set(sexpr.SymEmbed, sxpf.NewBuiltin(b.Name(), true, 3, -1, gen.makeGenerateEmbed(b.GetValue())))
- return gen
-}
+ tx *szenc.Transformer
+ 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()
+ }
+ linkZettel := func(obj sx.Object) sx.Object {
+ attr, assoc, rest := findA(obj)
+ if attr == nil {
+ return obj
+ }
+
+ hrefP := assoc.Assoc(shtml.SymAttrHref)
+ if hrefP == nil {
+ return obj
+ }
+ href, ok := sx.GetString(hrefP.Cdr())
+ if !ok {
+ return obj
+ }
+ zid, fragment, hasFragment := strings.Cut(href.GetValue(), "#")
+ u := builder.NewURLBuilder('h').SetZid(api.ZettelID(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)
+ }
+
+ rebind(th, sz.SymLinkZettel, linkZettel)
+ rebind(th, sz.SymLinkFound, linkZettel)
+ rebind(th, sz.SymLinkBased, func(obj sx.Object) sx.Object {
+ attr, assoc, rest := findA(obj)
+ if attr == nil {
+ return obj
+ }
+ hrefP := assoc.Assoc(shtml.SymAttrHref)
+ if hrefP == nil {
+ return obj
+ }
+ href, ok := sx.GetString(hrefP.Cdr())
+ if !ok {
+ return obj
+ }
+ 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)
+ })
+ rebind(th, sz.SymLinkQuery, func(obj sx.Object) sx.Object {
+ attr, assoc, rest := findA(obj)
+ if attr == nil {
+ return obj
+ }
+ hrefP := assoc.Assoc(shtml.SymAttrHref)
+ if hrefP == nil {
+ return obj
+ }
+ href, ok := sx.GetString(hrefP.Cdr())
+ if !ok {
+ return obj
+ }
+ ur, err := url.Parse(href.GetValue())
+ if err != nil {
+ return obj
+ }
+ q := ur.Query().Get(api.QueryKeyQuery)
+ if q == "" {
+ return obj
+ }
+ u := builder.NewURLBuilder('h').AppendQuery(q)
+ assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())))
+ return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
+ })
+ rebind(th, sz.SymLinkExternal, func(obj sx.Object) sx.Object {
+ attr, assoc, rest := findA(obj)
+ if attr == nil {
+ return obj
+ }
+ assoc = assoc.Cons(sx.Cons(shtml.SymAttrClass, sx.MakeString("external"))).
+ Cons(sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank"))).
+ Cons(sx.Cons(shtml.SymAttrRel, sx.MakeString("noopener noreferrer")))
+ return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
+ })
+ rebind(th, sz.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 := api.ZettelID(src.GetValue())
+ if !zid.IsValid() {
+ 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: szenc.NewTransformer(),
+ 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()
+ })
+}
+
+// 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{
api.KeyCopyright: "copyright",
api.KeyLicense: "license",
}
-func (g *htmlGenerator) MetaString(m *meta.Meta, evalMeta encoder.EvalMetaFunc) string {
- ignore := strfun.NewSet(api.KeyTitle, api.KeyLang)
- var buf bytes.Buffer
-
- if tags, ok := m.Get(api.KeyAllTags); ok {
- writeMetaTags(&buf, tags)
- ignore.Set(api.KeyAllTags)
- ignore.Set(api.KeyTags)
- } else if tags, ok = m.Get(api.KeyTags); ok {
- writeMetaTags(&buf, tags)
- ignore.Set(api.KeyTags)
- }
-
- for _, p := range m.ComputedPairs() {
- key := p.Key
- if ignore.Has(key) {
- continue
- }
- if altKey, found := mapMetaKey[key]; found {
- buf.WriteString(` \n")
- }
- return buf.String()
-}
-func writeMetaTags(buf *bytes.Buffer, tags string) {
- buf.WriteString(` \n")
-}
-
-// BlocksString encodes a block slice.
-func (g *htmlGenerator) BlocksString(bs *ast.BlockSlice) (string, error) {
- if bs == nil || len(*bs) == 0 {
- return "", nil
- }
- lst := sexprenc.GetSexpr(bs)
- var buf bytes.Buffer
- g.env.ReplaceWriter(&buf)
- sxpf.Eval(g.env, lst)
- if g.env.GetError() == nil {
- g.env.WriteEndnotes()
- }
- g.env.ReplaceWriter(nil)
- return buf.String(), g.env.GetError()
-}
-
-// InlinesString writes an inline slice to the writer
-func (g *htmlGenerator) InlinesString(is *ast.InlineSlice) (string, error) {
- if is == nil || len(*is) == 0 {
- return "", nil
- }
- return html.EvaluateInline(g.env, sexprenc.GetSexpr(is), true, false), nil
-}
-
-func (g *htmlGenerator) generateTag(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
- if !sxpf.IsNil(args) {
- env := senv.(*html.EncEnvironment)
- s := env.GetString(args)
- if env.IgnoreLinks() {
- env.WriteEscaped(s)
- } else {
- u := g.builder.NewURLBuilder('h').AppendQuery(api.KeyAllTags, "#"+strings.ToLower(s))
- env.WriteStrings(`#`)
- env.WriteEscaped(s)
- env.WriteString(" ")
- }
- }
- return nil, nil
-}
-
-func (g *htmlGenerator) generateLinkZettel(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
- env := senv.(*html.EncEnvironment)
- if a, refValue, ok := html.PrepareLink(env, args); ok {
- zid, fragment, hasFragment := strings.Cut(refValue, "#")
- u := g.builder.NewURLBuilder('h').SetZid(api.ZettelID(zid))
- if hasFragment {
- u = u.SetFragment(fragment)
- }
- html.WriteLink(env, args, a.Set("href", u.String()), refValue, "")
- }
- return nil, nil
-}
-
-func (g *htmlGenerator) generateLinkBased(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
- env := senv.(*html.EncEnvironment)
- if a, refValue, ok := html.PrepareLink(env, args); ok {
- u := g.builder.NewURLBuilder('/').SetRawLocal(refValue)
- html.WriteLink(env, args, a.Set("href", u.String()), refValue, "")
- }
- return nil, nil
-}
-
-func (g *htmlGenerator) generateLinkSearch(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
- env := senv.(*html.EncEnvironment)
- if a, refValue, ok := html.PrepareLink(env, args); ok {
- searchExpr := search.Parse(refValue).String()
- u := g.builder.NewURLBuilder('h').AppendSearch(searchExpr)
- html.WriteLink(env, args, a.Set("href", u.String()), refValue, "")
- }
- return nil, nil
-}
-
-func (g *htmlGenerator) generateLinkExternal(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
- env := senv.(*html.EncEnvironment)
- if a, refValue, ok := html.PrepareLink(env, args); ok {
- a = a.Set("href", refValue).
- AddClass("external").
- Set("target", "_blank").
- Set("rel", "noopener noreferrer")
- html.WriteLink(env, args, a, refValue, g.extMarker)
- }
- return nil, nil
-}
-
-func (g *htmlGenerator) makeGenerateEmbed(oldFn sxpf.BuiltinFn) sxpf.BuiltinFn {
- return func(senv sxpf.Environment, args *sxpf.Pair, arity int) (sxpf.Value, error) {
- env := senv.(*html.EncEnvironment)
- ref := env.GetPair(args.GetTail())
- refValue := env.GetString(ref.GetTail())
- zid := api.ZettelID(refValue)
- if !zid.IsValid() {
- return oldFn(senv, args, arity)
- }
- u := g.builder.NewURLBuilder('z').SetZid(zid)
- env.WriteImageWithSource(args, u.String())
- return nil, nil
- }
+func (g *htmlGenerator) MetaSxn(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
+ tm := g.tx.GetMeta(m, evalMeta)
+ env := shtml.MakeEnvironment(g.lang)
+ hm, err := g.th.Evaluate(tm, &env)
+ if err != nil {
+ return nil
+ }
+
+ ignore := strfun.NewSet(api.KeyTitle, api.KeyLang)
+ metaMap := make(map[string]*sx.Pair, m.Length())
+ if tags, ok := m.Get(api.KeyTags); ok {
+ metaMap[api.KeyTags] = g.transformMetaTags(tags)
+ ignore.Set(api.KeyTags)
+ }
+
+ for elem := hm; elem != nil; elem = elem.Tail() {
+ mlst, isPair := sx.GetPair(elem.Car())
+ if !isPair {
+ continue
+ }
+ att, isPair := sx.GetPair(mlst.Tail().Car())
+ if !isPair {
+ continue
+ }
+ if !att.Car().IsEqual(g.symAt) {
+ continue
+ }
+ a := make(attrs.Attributes, 32)
+ for aelem := att.Tail(); aelem != nil; aelem = aelem.Tail() {
+ if p, ok := sx.GetPair(aelem.Car()); ok {
+ key := p.Car()
+ val := p.Cdr()
+ if tail, isTail := sx.GetPair(val); isTail {
+ val = tail.Car()
+ }
+ a = a.Set(sz.GoValue(key), sz.GoValue(val))
+ }
+ }
+ name, found := a.Get("name")
+ if !found || ignore.Has(name) {
+ continue
+ }
+
+ newName, found := mapMetaKey[name]
+ if !found {
+ continue
+ }
+ a = a.Set("name", newName)
+ metaMap[newName] = g.th.EvaluateMeta(a)
+ }
+ result := sx.Nil()
+ keys := maps.Keys(metaMap)
+ for i := len(keys) - 1; i >= 0; i-- {
+ result = result.Cons(metaMap[keys[i]])
+ }
+ return result
+}
+
+func (g *htmlGenerator) transformMetaTags(tags string) *sx.Pair {
+ var sb strings.Builder
+ for i, val := range meta.ListFromValue(tags) {
+ if i > 0 {
+ sb.WriteString(", ")
+ }
+ sb.WriteString(strings.TrimPrefix(val, "#"))
+ }
+ metaTags := sb.String()
+ if len(metaTags) == 0 {
+ return nil
+ }
+ return g.th.EvaluateMeta(attrs.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, g.th.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
+ }
+ sx := g.tx.GetSz(is)
+ env := shtml.MakeEnvironment(g.lang)
+ sh, err := g.th.Evaluate(sx, &env)
+ if err != nil {
+ return nil
+ }
+ return sh
}
Index: web/adapter/webui/htmlmeta.go
==================================================================
--- web/adapter/webui/htmlmeta.go
+++ web/adapter/webui/htmlmeta.go
@@ -1,191 +1,174 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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"
- "fmt"
- "io"
- "net/url"
- "time"
- "zettelstore.de/c/api"
- "zettelstore.de/c/html"
+ "t73f.de/r/sx"
+ "t73f.de/r/sxwebs/sxhtml"
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/shtml"
"zettelstore.de/z/ast"
"zettelstore.de/z/box"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/parser"
"zettelstore.de/z/usecase"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
)
-var space = []byte{' '}
-
-type evalMetadataFunc = func(string) ast.InlineSlice
-
func (wui *WebUI) writeHTMLMetaValue(
- w io.Writer,
key, value string,
getTextTitle getTextTitleFunc,
evalMetadata evalMetadataFunc,
gen *htmlGenerator,
-) {
+) sx.Object {
switch kt := meta.Type(key); kt {
case meta.TypeCredential:
- writeCredential(w, value)
+ return sx.MakeString(value)
case meta.TypeEmpty:
- writeEmpty(w, value)
+ return sx.MakeString(value)
case meta.TypeID:
- wui.writeIdentifier(w, value, getTextTitle)
+ return wui.transformIdentifier(value, getTextTitle)
case meta.TypeIDSet:
- wui.writeIdentifierSet(w, meta.ListFromValue(value), getTextTitle)
+ return wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle)
case meta.TypeNumber:
- wui.writeNumber(w, key, value)
+ return wui.transformKeyValueText(key, value, value)
case meta.TypeString:
- writeString(w, value)
+ return sx.MakeString(value)
case meta.TypeTagSet:
- wui.writeTagSet(w, key, meta.ListFromValue(value))
+ return wui.transformTagSet(key, meta.ListFromValue(value))
case meta.TypeTimestamp:
if ts, ok := meta.TimeValue(value); ok {
- writeTimestamp(w, ts)
+ 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:
- writeURL(w, value)
+ return wui.url2html(sx.MakeString(value))
case meta.TypeWord:
- wui.writeWord(w, key, value)
- case meta.TypeWordSet:
- wui.writeWordSet(w, key, meta.ListFromValue(value))
+ return wui.transformKeyValueText(key, value, value)
case meta.TypeZettelmarkup:
- io.WriteString(w, encodeZmkMetadata(value, evalMetadata, gen))
+ return wui.transformZmkMetadata(value, evalMetadata, gen)
default:
- html.Escape(w, value)
- fmt.Fprintf(w, " (Unhandled type: %v, key: %v) ", kt, key)
+ return sx.MakeList(shtml.SymSTRONG, sx.MakeString("Unhandled type: "), sx.MakeString(kt.Name))
}
}
-func writeCredential(w io.Writer, val string) { html.Escape(w, val) }
-func writeEmpty(w io.Writer, val string) { html.Escape(w, val) }
-
-func (wui *WebUI) writeIdentifier(w io.Writer, val string, getTextTitle getTextTitleFunc) {
+func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sx.Object {
+ text := sx.MakeString(val)
zid, err := id.Parse(val)
if err != nil {
- html.Escape(w, val)
- return
+ return text
}
title, found := getTextTitle(zid)
switch {
case found > 0:
- ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String()))
- if title == "" {
- fmt.Fprintf(w, "%v ", ub, zid)
- } else {
- fmt.Fprintf(w, "%v ", ub, title, zid)
- }
- case found == 0:
- fmt.Fprintf(w, "%v ", val)
- case found < 0:
- io.WriteString(w, val)
- }
-}
-
-func (wui *WebUI) writeIdentifierSet(w io.Writer, vals []string, getTextTitle getTextTitleFunc) {
- for i, val := range vals {
- if i > 0 {
- w.Write(space)
- }
- wui.writeIdentifier(w, val, getTextTitle)
- }
-}
-
-func (wui *WebUI) writeNumber(w io.Writer, key, val string) {
- wui.writeLink(w, key, val, val)
-}
-
-func writeString(w io.Writer, val string) { html.Escape(w, val) }
-
-func (wui *WebUI) writeTagSet(w io.Writer, key string, tags []string) {
- for i, tag := range tags {
- if i > 0 {
- w.Write(space)
- }
- wui.writeLink(w, key, tag, tag)
- }
-}
-
-func writeTimestamp(w io.Writer, ts time.Time) {
- io.WriteString(w, ts.Format("2006-01-02 15:04:05"))
-}
-
-func writeURL(w io.Writer, val string) {
- u, err := url.Parse(val)
- if err != nil {
- html.Escape(w, val)
- return
- }
- fmt.Fprintf(w, "", u, htmlAttrNewWindow(true))
- html.Escape(w, val)
- io.WriteString(w, " ")
-}
-
-func (wui *WebUI) writeWord(w io.Writer, key, word string) {
- wui.writeLink(w, key, word, word)
-}
-
-func (wui *WebUI) writeWordSet(w io.Writer, key string, words []string) {
- for i, word := range words {
- if i > 0 {
- w.Write(space)
- }
- wui.writeWord(w, key, word)
- }
-}
-
-func (wui *WebUI) writeLink(w io.Writer, key, value, text string) {
- fmt.Fprintf(w, "", wui.NewURLBuilder('h'), url.QueryEscape(key), url.QueryEscape(value))
- html.Escape(w, text)
- io.WriteString(w, " ")
+ ub := wui.NewURLBuilder('h').SetZid(zid.ZettelID())
+ 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
+ }
+}
+
+func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) *sx.Pair {
+ if len(vals) == 0 {
+ return nil
+ }
+ var space = sx.MakeString(" ")
+ text := make(sx.Vector, 0, 2*len(vals))
+ for _, val := range vals {
+ text = append(text, space, wui.transformIdentifier(val, getTextTitle))
+ }
+ return sx.MakeList(text[1:]...).Cons(shtml.SymSPAN)
+}
+
+func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair {
+ if len(tags) == 0 {
+ return nil
+ }
+ var space = sx.MakeString(" ")
+ text := make(sx.Vector, 0, 2*len(tags)+2)
+ for _, tag := range tags {
+ text = append(text, space, wui.transformKeyValueText(key, tag, tag))
+ }
+ if len(tags) > 1 {
+ text = append(text, space, wui.transformKeyValuesText(key, tags, "(all)"))
+ }
+ return sx.MakeList(text[1:]...).Cons(shtml.SymSPAN)
+}
+
+func (wui *WebUI) transformKeyValueText(key, value, text string) *sx.Pair {
+ ub := wui.NewURLBuilder('h').AppendQuery(key + api.SearchOperatorHas + 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 evalMetadataFunc = func(string) ast.InlineSlice
+
+func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc {
+ return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) }
}
type getTextTitleFunc func(id.Zid) (string, int)
-func (wui *WebUI) makeGetTextTitle(
- ctx context.Context,
- getMeta usecase.GetMeta, evaluate *usecase.Evaluate,
-) getTextTitleFunc {
+func (wui *WebUI) makeGetTextTitle(ctx context.Context, getZettel usecase.GetZettel) getTextTitleFunc {
return func(zid id.Zid) (string, int) {
- m, err := getMeta.Run(box.NoEnrichContext(ctx), zid)
+ z, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
if err != nil {
if errors.Is(err, &box.ErrNotAllowed{}) {
return "", -1
}
return "", 0
}
- return wui.encodeTitleAsText(ctx, m, evaluate), 1
+ return parser.NormalizedSpacedText(z.Meta.GetTitle()), 1
}
}
-func (wui *WebUI) encodeTitleAsText(ctx context.Context, m *meta.Meta, evaluate *usecase.Evaluate) string {
- is := evaluate.RunMetadata(ctx, m.GetTitle())
- result, err := encodeInlinesText(&is, wui.gentext)
- if err != nil {
- return err.Error()
- }
- return result
-}
-
-func encodeZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) string {
+func (wui *WebUI) transformZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) sx.Object {
is := evalMetadata(value)
- result, err := gen.InlinesString(&is)
- if err != nil {
- return err.Error()
- }
- return result
+ return gen.InlinesSxHTML(&is).Cons(shtml.SymSPAN)
}
Index: web/adapter/webui/lists.go
==================================================================
--- web/adapter/webui/lists.go
+++ web/adapter/webui/lists.go
@@ -1,271 +1,266 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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"
- "net/http"
- "net/url"
- "sort"
- "strconv"
-
- "zettelstore.de/c/api"
- "zettelstore.de/z/ast"
- "zettelstore.de/z/box"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
- "zettelstore.de/z/search"
- "zettelstore.de/z/usecase"
- "zettelstore.de/z/web/adapter"
-)
-
-// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of
-// zettel as HTML.
-func (wui *WebUI) MakeListHTMLMetaHandler(
- listMeta usecase.ListMeta,
- listRole usecase.ListRoles,
- listTags usecase.ListTags,
- evaluate *usecase.Evaluate,
-) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- query := r.URL.Query()
- switch query.Get("_l") {
- case "r":
- wui.renderRolesList(w, r, listRole)
- case "t":
- wui.renderTagsList(w, r, listTags)
- default:
- wui.renderZettelList(w, r, listMeta, evaluate)
- }
- }
-}
-
-func (wui *WebUI) renderZettelList(
- w http.ResponseWriter, r *http.Request,
- listMeta usecase.ListMeta, evaluate *usecase.Evaluate,
-) {
- query := r.URL.Query()
- s := adapter.GetSearch(query)
- ctx := r.Context()
- if !s.EnrichNeeded() {
- ctx = box.NoEnrichContext(ctx)
- }
- metaList, err := listMeta.Run(ctx, s)
- if err != nil {
- wui.reportError(ctx, w, err)
- return
- }
- user := wui.getUser(ctx)
- metas := wui.buildHTMLMetaList(metaList, func(val string) ast.InlineSlice { return evaluate.RunMetadataNoLink(ctx, val) })
- var base baseData
- wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base)
- wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct {
- Title string
- SearchURL string
- SearchValue string
- QueryKeySearch string
- Metas []simpleLink
- }{
- Title: wui.listTitleSearch(s),
- SearchURL: base.SearchURL,
- SearchValue: s.String(),
- QueryKeySearch: base.QueryKeySearch,
- Metas: metas,
- })
-}
-
-type roleInfo struct {
- Text string
- URL string
-}
-
-func (wui *WebUI) renderRolesList(w http.ResponseWriter, r *http.Request, listRole usecase.ListRoles) {
- ctx := r.Context()
- roleArrangement, err := listRole.Run(ctx)
- if err != nil {
- wui.reportError(ctx, w, err)
- return
- }
- roleList := roleArrangement.Counted()
- roleList.SortByName()
-
- roleInfos := make([]roleInfo, len(roleList))
- for i, role := range roleList {
- roleInfos[i] = roleInfo{role.Name, wui.NewURLBuilder('h').AppendQuery("role", role.Name).String()}
- }
-
- user := wui.getUser(ctx)
- var base baseData
- wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base)
- wui.renderTemplate(ctx, w, id.RolesTemplateZid, &base, struct {
- Roles []roleInfo
- }{
- Roles: roleInfos,
- })
-}
-
-type countInfo struct {
- Count string
- URL string
-}
-
-type tagInfo struct {
- Name string
- URL string
- iCount int
- Count string
- Size string
-}
-
-const fontSizes = 6 // Must be the number of CSS classes zs-font-size-* in base.css
-
-func (wui *WebUI) renderTagsList(w http.ResponseWriter, r *http.Request, listTags usecase.ListTags) {
- ctx := r.Context()
- iMinCount, err := strconv.Atoi(r.URL.Query().Get("min"))
- if err != nil || iMinCount < 0 {
- iMinCount = 0
- }
- tagData, err := listTags.Run(ctx, iMinCount)
- if err != nil {
- wui.reportError(ctx, w, err)
- return
- }
-
- user := wui.getUser(ctx)
- tagsList := make([]tagInfo, 0, len(tagData))
- countMap := make(map[int]int)
- baseTagListURL := wui.NewURLBuilder('h')
- for tag, ml := range tagData {
- count := len(ml)
- countMap[count]++
- tagsList = append(
- tagsList,
- tagInfo{tag, baseTagListURL.AppendQuery(api.KeyAllTags, tag).String(), count, "", ""})
- baseTagListURL.ClearQuery()
- }
- sort.Slice(tagsList, func(i, j int) bool { return tagsList[i].Name < tagsList[j].Name })
-
- countList := make([]int, 0, len(countMap))
- for count := range countMap {
- countList = append(countList, count)
- }
- sort.Ints(countList)
- for pos, count := range countList {
- countMap[count] = (pos * fontSizes) / len(countList)
- }
- for i := 0; i < len(tagsList); i++ {
- count := tagsList[i].iCount
- tagsList[i].Count = strconv.Itoa(count)
- tagsList[i].Size = strconv.Itoa(countMap[count])
- }
-
- var base baseData
- wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base)
- minCounts := make([]countInfo, 0, len(countList))
- for _, c := range countList {
- sCount := strconv.Itoa(c)
- minCounts = append(minCounts, countInfo{sCount, base.ListTagsURL + "&min=" + sCount})
- }
-
- wui.renderTemplate(ctx, w, id.TagsTemplateZid, &base, struct {
- ListTagsURL string
- MinCounts []countInfo
- Tags []tagInfo
- }{
- ListTagsURL: base.ListTagsURL,
- MinCounts: minCounts,
- Tags: tagsList,
- })
-}
-
-// MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context".
-func (wui *WebUI) MakeZettelContextHandler(getContext usecase.ZettelContext, evaluate *usecase.Evaluate) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- zid, err := id.Parse(r.URL.Path[1:])
- if err != nil {
- wui.reportError(ctx, w, box.ErrNotFound)
- return
- }
- q := r.URL.Query()
- dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir))
- depth := getIntParameter(q, api.QueryKeyDepth, 5)
- limit := getIntParameter(q, api.QueryKeyLimit, 200)
- metaList, err := getContext.Run(ctx, zid, dir, depth, limit)
- if err != nil {
- wui.reportError(ctx, w, err)
- return
- }
- apiZid := api.ZettelID(zid.String())
- metaLinks := wui.buildHTMLMetaList(metaList, func(val string) ast.InlineSlice { return evaluate.RunMetadataNoLink(ctx, val) })
- depths := []string{"2", "3", "4", "5", "6", "7", "8", "9", "10"}
- depthLinks := make([]simpleLink, len(depths))
- depthURL := wui.NewURLBuilder('k').SetZid(apiZid)
- for i, depth := range depths {
- depthURL.ClearQuery()
- switch dir {
- case usecase.ZettelContextBackward:
- depthURL.AppendQuery(api.QueryKeyDir, api.DirBackward)
- case usecase.ZettelContextForward:
- depthURL.AppendQuery(api.QueryKeyDir, api.DirForward)
- }
- depthURL.AppendQuery(api.QueryKeyDepth, depth)
- depthLinks[i].Text = depth
- depthLinks[i].URL = depthURL.String()
- }
- var base baseData
- user := wui.getUser(ctx)
- wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base)
- wui.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct {
- Title string
- InfoURL string
- Depths []simpleLink
- Start simpleLink
- Metas []simpleLink
- }{
- Title: "Zettel Context",
- InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(),
- Depths: depthLinks,
- Start: metaLinks[0],
- Metas: metaLinks[1:],
- })
- }
-}
-
-func getIntParameter(q url.Values, key string, minValue int) int {
- val, ok := adapter.GetInteger(q, key)
- if !ok || val < 0 {
- return minValue
- }
- return val
-}
-
-func (wui *WebUI) listTitleSearch(s *search.Search) string {
- if s == nil {
- return wui.rtConfig.GetSiteName()
- }
- var buf bytes.Buffer
- s.PrintHuman(&buf)
- return buf.String()
-}
-
-// buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering.
-func (wui *WebUI) buildHTMLMetaList(metaList []*meta.Meta, evalMetadata evalMetadataFunc) []simpleLink {
- metas := make([]simpleLink, 0, len(metaList))
- encHTML := wui.getSimpleHTMLEncoder()
- for _, m := range metaList {
- metas = append(metas, simpleLink{
- Text: encodeZmkMetadata(m.GetTitle(), evalMetadata, encHTML),
- URL: wui.NewURLBuilder('h').SetZid(api.ZettelID(m.Zid.String())).String(),
- })
- }
- return metas
+ "context"
+ "io"
+ "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/shtml"
+ "zettelstore.de/z/ast"
+ "zettelstore.de/z/encoding/atom"
+ "zettelstore.de/z/encoding/rss"
+ "zettelstore.de/z/encoding/xml"
+ "zettelstore.de/z/evaluator"
+ "zettelstore.de/z/query"
+ "zettelstore.de/z/usecase"
+ "zettelstore.de/z/web/adapter"
+ "zettelstore.de/z/web/server"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
+
+// 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.HandlerFunc {
+ return 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(actions) > 0 {
+ if len(metaSeq) > 0 {
+ for _, act := range actions {
+ if act == api.RedirectAction {
+ ub := wui.NewURLBuilder('h').SetZid(metaSeq[0].Zid.ZettelID())
+ wui.redirectFound(w, r, ub)
+ return
+ }
+ }
+ }
+ switch actions[0] {
+ case api.AtomAction:
+ wui.renderAtom(w, q, metaSeq)
+ return
+ case api.RSSAction:
+ wui.renderRSS(ctx, w, q, metaSeq)
+ return
+ }
+ }
+
+ var content, endnotes *sx.Pair
+ numEntries := 0
+ if bn, cnt := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig); bn != nil {
+ enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, nil, api.KeyLang))
+ content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn})
+ if err != nil {
+ wui.reportError(ctx, w, err)
+ return
+ }
+ numEntries = cnt
+ }
+
+ user := server.GetUser(ctx)
+ env, rb := wui.createRenderEnv(
+ ctx, "list",
+ wui.rtConfig.Get(ctx, nil, api.KeyLang),
+ wui.rtConfig.GetSiteName(), user)
+ if q == nil {
+ rb.bindString("heading", sx.MakeString(wui.rtConfig.GetSiteName()))
+ } 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(api.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(api.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.ListTemplateZid, env)
+ } else {
+ err = rb.err
+ }
+ if err != nil {
+ wui.reportError(ctx, w, err)
+ }
+ }
+}
+
+func (wui *WebUI) transformTagZettelList(ctx context.Context, tagZettel *usecase.TagZettel, tags []string) (withZettel, withoutZettel *sx.Pair) {
+ slices.Reverse(tags)
+ for _, tag := range tags {
+ tag = meta.NormalizeTag(tag)
+ if _, err := tagZettel.Run(ctx, tag); err == nil {
+ u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyTag, tag)
+ withZettel = wui.prependZettelLink(withZettel, tag, u)
+ } else {
+ u := wui.NewURLBuilder('c').SetZid(api.ZidTemplateNewTag).AppendKVQuery(queryKeyAction, valueActionNew).AppendKVQuery(api.KeyTitle, tag)
+ withoutZettel = wui.prependZettelLink(withoutZettel, tag, u)
+ }
+ }
+ return withZettel, withoutZettel
+}
+
+func (wui *WebUI) transformRoleZettelList(ctx context.Context, roleZettel *usecase.RoleZettel, roles []string) (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, role)
+ withZettel = wui.prependZettelLink(withZettel, role, u)
+ } else {
+ u := wui.NewURLBuilder('c').SetZid(api.ZidTemplateNewRole).AppendKVQuery(queryKeyAction, valueActionNew).AppendKVQuery(api.KeyTitle, role)
+ withoutZettel = wui.prependZettelLink(withoutZettel, 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) renderRSS(ctx context.Context, w http.ResponseWriter, q *query.Query, ml []*meta.Meta) {
+ var rssConfig rss.Configuration
+ rssConfig.Setup(ctx, wui.rtConfig)
+ if actions := q.Actions(); len(actions) > 2 && actions[1] == api.TitleAction {
+ rssConfig.Title = strings.Join(actions[2:], " ")
+ }
+ data := rssConfig.Marshal(q, ml)
+
+ adapter.PrepareHeader(w, rss.ContentType)
+ w.WriteHeader(http.StatusOK)
+ var err error
+ if _, err = io.WriteString(w, xml.Header); err == nil {
+ _, err = w.Write(data)
+ }
+ if err != nil {
+ wui.log.Error().Err(err).Msg("unable to write RSS data")
+ }
+}
+
+func (wui *WebUI) renderAtom(w http.ResponseWriter, q *query.Query, ml []*meta.Meta) {
+ var atomConfig atom.Configuration
+ atomConfig.Setup(wui.rtConfig)
+ if actions := q.Actions(); len(actions) > 2 && actions[1] == api.TitleAction {
+ atomConfig.Title = strings.Join(actions[2:], " ")
+ }
+ data := atomConfig.Marshal(q, ml)
+
+ adapter.PrepareHeader(w, atom.ContentType)
+ w.WriteHeader(http.StatusOK)
+ var err error
+ if _, err = io.WriteString(w, xml.Header); err == nil {
+ _, err = w.Write(data)
+ }
+ if err != nil {
+ wui.log.Error().Err(err).Msg("unable to write Atom data")
+ }
+}
+
+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, tag)
+ if err != nil {
+ wui.reportError(ctx, w, err)
+ return true
+ }
+ wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(z.Meta.Zid.ZettelID()))
+ 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, role)
+ if err != nil {
+ wui.reportError(ctx, w, err)
+ return true
+ }
+ wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(z.Meta.Zid.ZettelID()))
+ return true
}
Index: web/adapter/webui/login.go
==================================================================
--- web/adapter/webui/login.go
+++ web/adapter/webui/login.go
@@ -1,25 +1,30 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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/api"
"zettelstore.de/z/auth"
- "zettelstore.de/z/domain/id"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
+ "zettelstore.de/z/zettel/id"
)
// MakeGetLoginOutHandler creates a new HTTP handler to display the HTML login view,
// or to execute a logout.
func (wui *WebUI) MakeGetLoginOutHandler() http.HandlerFunc {
@@ -33,19 +38,18 @@
wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false)
}
}
func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) {
- var base baseData
- wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), "Login", "", nil, &base)
- wui.renderTemplate(ctx, w, id.LoginTemplateZid, &base, struct {
- Title string
- Retry bool
- }{
- Title: base.Title,
- Retry: retry,
- })
+ env, rb := wui.createRenderEnv(ctx, "login", wui.rtConfig.Get(ctx, nil, api.KeyLang), "Login", nil)
+ rb.bindString("retry", sx.MakeBoolean(retry))
+ if rb.err == nil {
+ rb.err = wui.renderSxnTemplate(ctx, w, id.LoginTemplateZid, 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.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
@@ -57,11 +61,11 @@
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.KindHTML)
+ token, err := ucAuth.Run(ctx, r, ident, cred, wui.tokenLifetime, auth.KindwebUI)
if err != nil {
wui.reportError(ctx, w, err)
return
}
if token == nil {
ADDED web/adapter/webui/meta.go
Index: web/adapter/webui/meta.go
==================================================================
--- web/adapter/webui/meta.go
+++ web/adapter/webui/meta.go
@@ -0,0 +1,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 web/adapter/webui/meta_test.go
Index: web/adapter/webui/meta_test.go
==================================================================
--- web/adapter/webui/meta_test.go
+++ web/adapter/webui/meta_test.go
@@ -0,0 +1,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)
+ }
+ })
+ }
+}
Index: web/adapter/webui/rename_zettel.go
==================================================================
--- web/adapter/webui/rename_zettel.go
+++ web/adapter/webui/rename_zettel.go
@@ -1,89 +1,92 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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 (
"fmt"
"net/http"
"strings"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/box"
- "zettelstore.de/z/config"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
+ "zettelstore.de/z/web/server"
+ "zettelstore.de/z/zettel/id"
)
// MakeGetRenameZettelHandler creates a new HTTP handler to display the
// HTML rename view of a zettel.
-func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta, evaluate *usecase.Evaluate) http.HandlerFunc {
+func (wui *WebUI) MakeGetRenameZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- zid, err := id.Parse(r.URL.Path[1:])
+ path := r.URL.Path[1:]
+ zid, err := id.Parse(path)
if err != nil {
- wui.reportError(ctx, w, box.ErrNotFound)
+ wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
return
}
- m, err := getMeta.Run(ctx, zid)
+ z, err := getZettel.Run(ctx, zid)
if err != nil {
wui.reportError(ctx, w, err)
return
}
-
- getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate)
- incomingLinks := wui.encodeIncoming(m, getTextTitle)
- uselessFiles := retrieveUselessFiles(m)
-
- user := wui.getUser(ctx)
- var base baseData
- wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Rename Zettel "+zid.String(), "", user, &base)
- wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct {
- Zid string
- MetaPairs []meta.Pair
- HasIncoming bool
- Incoming []simpleLink
- HasUselessFiles bool
- UselessFiles []string
- }{
- Zid: zid.String(),
- MetaPairs: m.ComputedPairs(),
- HasIncoming: len(incomingLinks) > 0,
- Incoming: incomingLinks,
- HasUselessFiles: len(uselessFiles) > 0,
- UselessFiles: uselessFiles,
- })
+ m := z.Meta
+
+ user := server.GetUser(ctx)
+ env, rb := wui.createRenderEnv(
+ ctx, "rename",
+ wui.rtConfig.Get(ctx, nil, api.KeyLang), "Rename Zettel "+m.Zid.String(), user)
+ 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.RenameTemplateZid, env)
+ } else {
+ err = rb.err
+ }
+ if err != nil {
+ wui.reportError(ctx, w, err)
+ }
}
}
// MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel.
func (wui *WebUI) MakePostRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- curZid, err := id.Parse(r.URL.Path[1:])
+ path := r.URL.Path[1:]
+ curZid, err := id.Parse(path)
if err != nil {
- wui.reportError(ctx, w, box.ErrNotFound)
+ wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
return
}
if err = r.ParseForm(); err != nil {
+ wui.log.Trace().Err(err).Msg("unable to read rename zettel form")
wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form"))
return
}
- if formCurZid, err1 := id.Parse(
- r.PostFormValue("curzid")); err1 != nil || formCurZid != curZid {
+ formCurZidStr := r.PostFormValue("curzid")
+ if formCurZid, err1 := id.Parse(formCurZidStr); err1 != nil || formCurZid != curZid {
+ if err1 != nil {
+ wui.log.Trace().Str("formCurzid", formCurZidStr).Err(err1).Msg("unable to parse as zid")
+ } else if formCurZid != curZid {
+ wui.log.Trace().Zid(formCurZid).Zid(curZid).Msg("zid differ (form/url)")
+ }
wui.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form"))
return
}
formNewZid := strings.TrimSpace(r.PostFormValue("newzid"))
newZid, err := id.Parse(formNewZid)
@@ -95,8 +98,8 @@
if err = renameZettel.Run(r.Context(), curZid, newZid); err != nil {
wui.reportError(ctx, w, err)
return
}
- wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String())))
+ wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID()))
}
}
Index: web/adapter/webui/response.go
==================================================================
--- web/adapter/webui/response.go
+++ web/adapter/webui/response.go
@@ -1,23 +1,26 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 Detlef Stern
+// 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"
- "zettelstore.de/c/api"
+ "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 web/adapter/webui/sxn_code.go
Index: web/adapter/webui/sxn_code.go
==================================================================
--- web/adapter/webui/sxn_code.go
+++ web/adapter/webui/sxn_code.go
@@ -0,0 +1,111 @@
+//-----------------------------------------------------------------------------
+// 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/zsc/api"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
+
+func (wui *WebUI) loadAllSxnCodeZettel(ctx context.Context) (id.Digraph, *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.StartSxnZid, getMeta)
+ if dg == nil {
+ return nil, wui.rootBinding, nil
+ }
+ dg = dg.AddVertex(id.BaseSxnZid).AddEdge(id.StartSxnZid, id.BaseSxnZid)
+ dg = dg.AddVertex(id.PreludeSxnZid).AddEdge(id.BaseSxnZid, id.PreludeSxnZid)
+ dg = dg.TransitiveClosure(id.StartSxnZid)
+
+ 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) id.Digraph {
+ m, err := getMeta(ctx, startZid)
+ if err != nil {
+ return nil
+ }
+ var marked id.Set
+ stack := []*meta.Meta{m}
+ dg := id.Digraph(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)
+ if precursors, hasPrecursor := curr.GetList(api.KeyPrecursor); hasPrecursor && len(precursors) > 0 {
+ for _, pre := range precursors {
+ 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 web/adapter/webui/template.go
Index: web/adapter/webui/template.go
==================================================================
--- web/adapter/webui/template.go
+++ web/adapter/webui/template.go
@@ -0,0 +1,457 @@
+//-----------------------------------------------------------------------------
+// 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/shtml"
+ "zettelstore.de/z/box"
+ "zettelstore.de/z/collect"
+ "zettelstore.de/z/config"
+ "zettelstore.de/z/parser"
+ "zettelstore.de/z/web/adapter"
+ "zettelstore.de/z/web/server"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
+
+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.ZettelID())
+ 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.Defined, // 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("noopener 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-zettel-url", sx.MakeString(wui.listZettelURL))
+ rb.bindString("list-roles-url", sx.MakeString(wui.listRolesURL))
+ rb.bindString("list-tags-url", sx.MakeString(wui.listTagsURL))
+ 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.ZettelID()).String(), user.GetDefault(api.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 string) {
+ rb.bindString("meta-"+key, sx.MakeString(value))
+ if kt := meta.Type(key); kt.IsSet {
+ rb.bindString("set-meta-"+key, makeStringList(meta.ListFromValue(value)))
+ }
+}
+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) {
+ strZid := m.Zid.String()
+ apiZid := api.ZettelID(strZid)
+ newURLBuilder := wui.NewURLBuilder
+
+ rb.bindString("zid", sx.MakeString(strZid))
+ rb.bindString("web-url", sx.MakeString(newURLBuilder('h').SetZid(apiZid).String()))
+ if content != nil && wui.canWrite(ctx, user, m, *content) {
+ rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(apiZid).String()))
+ }
+ rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(apiZid).String()))
+ if wui.canCreate(ctx, user) {
+ if content != nil && !content.IsBinary() {
+ rb.bindString("copy-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String()))
+ }
+ rb.bindString("version-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String()))
+ rb.bindString("child-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionChild).String()))
+ rb.bindString("folge-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String()))
+ }
+ if wui.canRename(ctx, user, m) {
+ rb.bindString("rename-url", sx.MakeString(newURLBuilder('b').SetZid(apiZid).String()))
+ }
+ if wui.canDelete(ctx, user, m) {
+ rb.bindString("delete-url", sx.MakeString(newURLBuilder('d').SetZid(apiZid).String()))
+ }
+ if val, found := m.Get(api.KeyUselessFiles); found {
+ rb.bindString("useless", sx.Cons(sx.MakeString(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(api.KeyTitle, m.GetDefault(api.KeyTitle, ""))
+ rb.bindKeyValue(api.KeyRole, m.GetDefault(api.KeyRole, ""))
+ rb.bindKeyValue(api.KeyTags, m.GetDefault(api.KeyTags, ""))
+ rb.bindKeyValue(api.KeySyntax, m.GetDefault(api.KeySyntax, meta.DefaultSyntax))
+ var metaPairs sx.ListBuilder
+ for _, p := range m.ComputedPairs() {
+ key, value := p.Key, p.Value
+ metaPairs.Add(sx.Cons(sx.MakeString(key), sx.MakeString(value)))
+ rb.bindKeyValue(key, value)
+ }
+ rb.bindString("metapairs", metaPairs.List())
+}
+
+func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) (lst *sx.Pair) {
+ if !wui.canCreate(ctx, user) {
+ return nil
+ }
+ ctx = box.NoEnrichContext(ctx)
+ menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid)
+ if err != nil {
+ return nil
+ }
+ refs := collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig))
+ for i := len(refs) - 1; i >= 0; i-- {
+ zid, err2 := id.Parse(refs[i].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(parser.NormalizedSpacedText(z.Meta.GetTitle()))
+ link := sx.MakeString(wui.NewURLBuilder('c').SetZid(zid.ZettelID()).
+ AppendKVQuery(queryKeyAction, valueActionNew).String())
+
+ lst = lst.Cons(sx.Cons(text, link))
+ }
+ return lst
+}
+func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair {
+ if footerZid, err := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyFooterZettel)); err == nil {
+ if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil {
+ htmlEnc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang)).SetUnique("footer-")
+ if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.Ast); 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.BaseTemplateZid, 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) {
+ 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", api.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.ErrorTemplateZid, 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,
+ `
+
+Internal server error
+
+Internal server error
+When generating error code %d with message:
%v an error occured:
%v
+
+`, code, text, errSx)
+}
+
+func makeStringList(sl []string) *sx.Pair {
+ if len(sl) == 0 {
+ return nil
+ }
+ result := sx.Nil()
+ for i := len(sl) - 1; i >= 0; i-- {
+ result = result.Cons(sx.MakeString(sl[i]))
+ }
+ return result
+}
Index: web/adapter/webui/webui.go
==================================================================
--- web/adapter/webui/webui.go
+++ web/adapter/webui/webui.go
@@ -1,41 +1,42 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 Detlef Stern
+// 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 (
- "bytes"
"context"
"net/http"
- "strings"
"sync"
"time"
- "zettelstore.de/c/api"
+ "t73f.de/r/sx"
+ "t73f.de/r/sx/sxeval"
+ "t73f.de/r/sxwebs/sxhtml"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/auth"
"zettelstore.de/z/box"
- "zettelstore.de/z/collect"
"zettelstore.de/z/config"
- "zettelstore.de/z/domain"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
- "zettelstore.de/z/encoder/textenc"
"zettelstore.de/z/kernel"
"zettelstore.de/z/logger"
- "zettelstore.de/z/parser"
- "zettelstore.de/z/template"
+ "zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
"zettelstore.de/z/web/server"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
)
// WebUI holds all data for delivering the web ui.
type WebUI struct {
log *logger.Logger
@@ -45,17 +46,14 @@
rtConfig config.Config
token auth.TokenManager
box webuiBox
policy auth.Policy
- gentext *textenc.Encoder
+ evalZettel *usecase.Evaluate
mxCache sync.RWMutex
- templateCache map[id.Zid]*template.Template
-
- mxRoleCSSMap sync.RWMutex
- roleCSSMap map[string]id.Zid
+ templateCache map[id.Zid]sxeval.Expr
tokenLifetime time.Duration
cssBaseURL string
cssUserURL string
homeURL string
@@ -65,25 +63,36 @@
refreshURL string
withAuth bool
loginURL string
logoutURL string
searchURL string
+ createNewURL string
+
+ rootBinding *sxeval.Binding
+ mxZettelBinding sync.Mutex
+ zettelBinding *sxeval.Binding
+ dag id.Digraph
+ 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(ctx context.Context) bool
- GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
- GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
- CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool
- AllowRenameZettel(ctx context.Context, zid id.Zid) bool
- CanDeleteZettel(ctx context.Context, zid id.Zid) bool
+ 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
+ AllowRenameZettel(context.Context, id.Zid) 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) *WebUI {
+ 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,
@@ -90,113 +99,83 @@
authz: authz,
token: token,
box: mgr,
policy: pol,
- gentext: textenc.Create(),
+ 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(api.ZidBaseCSS).String(),
cssUserURL: ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(),
homeURL: ab.NewURLBuilder('/').String(),
listZettelURL: ab.NewURLBuilder('h').String(),
- listRolesURL: ab.NewURLBuilder('h').AppendQuery("_l", "r").String(),
- listTagsURL: ab.NewURLBuilder('h').AppendQuery("_l", "t").String(),
- refreshURL: ab.NewURLBuilder('g').AppendQuery("_c", "r").String(),
+ listRolesURL: ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyRole).String(),
+ listTagsURL: ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyTags).String(),
+ refreshURL: ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(),
withAuth: authz.WithAuth(),
loginURL: loginoutBase.String(),
- logoutURL: loginoutBase.AppendQuery("logout", "").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
}
+
+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 || ci.Zid == id.BaseTemplateZid {
- wui.templateCache = make(map[id.Zid]*template.Template, len(wui.templateCache))
+ if ci.Reason == box.OnReload {
+ clear(wui.templateCache)
} else {
delete(wui.templateCache, ci.Zid)
}
wui.mxCache.Unlock()
- wui.mxRoleCSSMap.Lock()
- if ci.Reason == box.OnReload || ci.Zid == id.RoleCSSMapZid {
- wui.roleCSSMap = nil
- }
- wui.mxRoleCSSMap.Unlock()
-}
-
-func (wui *WebUI) cacheSetTemplate(zid id.Zid, t *template.Template) {
- wui.mxCache.Lock()
- wui.templateCache[zid] = t
+
+ 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) cacheGetTemplate(zid id.Zid) (*template.Template, bool) {
- wui.mxCache.RLock()
- t, ok := wui.templateCache[zid]
- wui.mxCache.RUnlock()
- return t, ok
-}
-
-func (wui *WebUI) retrieveCSSZidFromRole(ctx context.Context, m meta.Meta) (id.Zid, error) {
- wui.mxRoleCSSMap.RLock()
- if wui.roleCSSMap == nil {
- wui.mxRoleCSSMap.RUnlock()
- wui.mxRoleCSSMap.Lock()
- mMap, err := wui.box.GetMeta(ctx, id.RoleCSSMapZid)
- if err == nil {
- wui.roleCSSMap = createRoleCSSMap(mMap)
- }
- wui.mxRoleCSSMap.Unlock()
- if err != nil {
- return id.Invalid, err
- }
- wui.mxRoleCSSMap.RLock()
- }
-
- defer wui.mxRoleCSSMap.RUnlock()
- if role, found := m.Get("css-role"); found {
- if result, found2 := wui.roleCSSMap[role]; found2 {
- return result, nil
- }
- }
- if role, found := m.Get(api.KeyRole); found {
- if result, found2 := wui.roleCSSMap[role]; found2 {
- return result, nil
- }
- }
- return id.Invalid, nil
-}
-
-func createRoleCSSMap(mMap *meta.Meta) map[string]id.Zid {
- result := make(map[string]id.Zid)
- for _, p := range mMap.PairsRest() {
- key := p.Key
- if len(key) < 9 || !strings.HasPrefix(key, "css-") || !strings.HasSuffix(key, "-zid") {
- continue
- }
- zid, err2 := id.Parse(p.Value)
- if err2 != nil {
- continue
- }
- result[key[4:len(key)-4]] = zid
- }
- return result
+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 domain.Content) bool {
+ ctx context.Context, user, meta *meta.Meta, content zettel.Content) bool {
return wui.policy.CanWrite(user, meta, meta) &&
- wui.box.CanUpdateZettel(ctx, domain.Zettel{Meta: meta, Content: content})
+ wui.box.CanUpdateZettel(ctx, zettel.Zettel{Meta: meta, Content: content})
}
func (wui *WebUI) canRename(ctx context.Context, user, m *meta.Meta) bool {
return wui.policy.CanRename(user, m) && wui.box.AllowRenameZettel(ctx, m.Zid)
}
@@ -207,218 +186,13 @@
func (wui *WebUI) canRefresh(user *meta.Meta) bool {
return wui.policy.CanRefresh(user)
}
-func (wui *WebUI) getTemplate(
- ctx context.Context, templateID id.Zid) (*template.Template, error) {
- if t, ok := wui.cacheGetTemplate(templateID); ok {
- return t, nil
- }
- realTemplateZettel, err := wui.box.GetZettel(ctx, templateID)
- if err != nil {
- return nil, err
- }
- t, err := template.ParseString(realTemplateZettel.Content.AsString(), nil)
- if err == nil {
- // t.SetErrorOnMissing()
- wui.cacheSetTemplate(templateID, t)
- }
- return t, err
-}
-
-type simpleLink struct {
- Text string
- URL string
-}
-
-type baseData struct {
- Lang string
- MetaHeader string
- CSSBaseURL string
- CSSUserURL string
- CSSRoleURL string
- Title string
- HomeURL string
- WithUser bool
- WithAuth bool
- UserIsValid bool
- UserZettelURL string
- UserIdent string
- LoginURL string
- LogoutURL string
- ListZettelURL string
- ListRolesURL string
- ListTagsURL string
- CanRefresh bool
- RefreshURL string
- HasNewZettelLinks bool
- NewZettelLinks []simpleLink
- SearchURL string
- QueryKeySearch string
- Content string
- FooterHTML string
- DebugMode bool
-}
-
-func (wui *WebUI) makeBaseData(ctx context.Context, lang, title, roleCSSURL string, user *meta.Meta, data *baseData) {
- var userZettelURL string
- var userIdent string
-
- userIsValid := user != nil
- if userIsValid {
- userZettelURL = wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String()
- userIdent = user.GetDefault(api.KeyUserID, "")
- }
- newZettelLinks := wui.fetchNewTemplates(ctx, user)
-
- data.Lang = lang
- data.CSSBaseURL = wui.cssBaseURL
- data.CSSUserURL = wui.cssUserURL
- data.CSSRoleURL = roleCSSURL
- data.Title = title
- data.HomeURL = wui.homeURL
- data.WithAuth = wui.withAuth
- data.WithUser = data.WithAuth
- data.UserIsValid = userIsValid
- data.UserZettelURL = userZettelURL
- data.UserIdent = userIdent
- data.LoginURL = wui.loginURL
- data.LogoutURL = wui.logoutURL
- data.ListZettelURL = wui.listZettelURL
- data.ListRolesURL = wui.listRolesURL
- data.ListTagsURL = wui.listTagsURL
- data.CanRefresh = wui.canRefresh(user)
- data.RefreshURL = wui.refreshURL
- data.HasNewZettelLinks = len(newZettelLinks) > 0
- data.NewZettelLinks = newZettelLinks
- data.SearchURL = wui.searchURL
- data.QueryKeySearch = api.QueryKeySearch
- data.FooterHTML = wui.rtConfig.GetFooterHTML()
- data.DebugMode = wui.debug
-}
-
-func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator {
- return createGenerator(wui, "", false)
-}
-func (wui *WebUI) createZettelEncoder() *htmlGenerator {
- return createGenerator(wui, wui.rtConfig.GetMarkerExternal(), true)
-}
-
-// htmlAttrNewWindow returns HTML attribute string for opening a link in a new window.
-// If hasURL is false an empty string is returned.
-func htmlAttrNewWindow(hasURL bool) string {
- if hasURL {
- return " target=\"_blank\" ref=\"noopener noreferrer\""
- }
- return ""
-}
-
-func (wui *WebUI) fetchNewTemplates(ctx context.Context, user *meta.Meta) (result []simpleLink) {
- ctx = box.NoEnrichContext(ctx)
- if !wui.canCreate(ctx, user) {
- return nil
- }
- menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid)
- if err != nil {
- return nil
- }
- refs := collect.Order(parser.ParseZettel(menu, "", wui.rtConfig))
- for _, ref := range refs {
- zid, err2 := id.Parse(ref.URL.Path)
- if err2 != nil {
- continue
- }
- m, err2 := wui.box.GetMeta(ctx, zid)
- if err2 != nil {
- continue
- }
- if !wui.policy.CanRead(user, m) {
- continue
- }
- title := m.GetTitle()
- astTitle := parser.ParseMetadataNoLink(title)
- menuTitle, err2 := wui.getSimpleHTMLEncoder().InlinesString(&astTitle)
- if err2 != nil {
- menuTitle, err2 = encodeInlinesText(&astTitle, wui.gentext)
- if err2 != nil {
- menuTitle = title
- }
- }
- result = append(result, simpleLink{
- Text: menuTitle,
- URL: wui.NewURLBuilder('c').SetZid(api.ZettelID(m.Zid.String())).
- AppendQuery(queryKeyAction, valueActionNew).String(),
- })
- }
- return result
-}
-
-func (wui *WebUI) renderTemplate(
- ctx context.Context,
- w http.ResponseWriter,
- templateID id.Zid,
- base *baseData,
- data interface{}) {
- wui.renderTemplateStatus(ctx, w, http.StatusOK, templateID, base, data)
-}
-
-func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) {
- code, text := adapter.CodeMessageFromError(err)
- if code == http.StatusInternalServerError {
- wui.log.Error().Msg(err.Error())
- }
- user := wui.getUser(ctx)
- var base baseData
- wui.makeBaseData(ctx, api.ValueLangEN, "Error", "", user, &base)
- wui.renderTemplateStatus(ctx, w, code, id.ErrorTemplateZid, &base, struct {
- ErrorTitle string
- ErrorText string
- }{
- ErrorTitle: http.StatusText(code),
- ErrorText: text,
- })
-}
-
-func (wui *WebUI) renderTemplateStatus(
- ctx context.Context,
- w http.ResponseWriter,
- code int,
- templateID id.Zid,
- base *baseData,
- data interface{}) {
-
- bt, err := wui.getTemplate(ctx, id.BaseTemplateZid)
- if err != nil {
- wui.log.IfErr(err).Zid(id.BaseTemplateZid).Msg("Unable to get template")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
- t, err := wui.getTemplate(ctx, templateID)
- if err != nil {
- wui.log.IfErr(err).Zid(templateID).Msg("Unable to get template")
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
- if user := wui.getUser(ctx); user != nil {
- if tok, err1 := wui.token.GetToken(user, wui.tokenLifetime, auth.KindHTML); err1 == nil {
- wui.setToken(w, tok)
- }
- }
- var content bytes.Buffer
- err = t.Render(&content, data)
- if err == nil {
- wui.prepareAndWriteHeader(w, code)
- base.Content = content.String()
- err = bt.Render(w, base)
- }
- if err != nil {
- wui.log.IfErr(err).Msg("Unable to write HTML via template")
- }
-}
-
-func (wui *WebUI) getUser(ctx context.Context) *meta.Meta { return wui.ab.GetUser(ctx) }
+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.
ADDED web/content/content.go
Index: web/content/content.go
==================================================================
--- web/content/content.go
+++ web/content/content.go
@@ -0,0 +1,122 @@
+//-----------------------------------------------------------------------------
+// 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"
+ "zettelstore.de/z/zettel"
+ "zettelstore.de/z/zettel/meta"
+)
+
+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.SyntaxCSS: "text/css; charset=utf-8",
+ meta.SyntaxDraw: PlainText,
+ meta.SyntaxGif: mimeGIF,
+ meta.SyntaxHTML: mimeHTML,
+ meta.SyntaxJPEG: mimeJPEG,
+ meta.SyntaxJPG: mimeJPEG,
+ meta.SyntaxMarkdown: mimeMarkdown,
+ meta.SyntaxMD: mimeMarkdown,
+ meta.SyntaxNone: "",
+ meta.SyntaxPlain: PlainText,
+ meta.SyntaxPNG: mimePNG,
+ meta.SyntaxSVG: "image/svg+xml",
+ meta.SyntaxSxn: SXPF,
+ meta.SyntaxText: PlainText,
+ meta.SyntaxTxt: PlainText,
+ meta.SyntaxWebp: mimeWEBP,
+ meta.SyntaxZmk: "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.SyntaxGif,
+ mimeJPEG: meta.SyntaxJPEG,
+ mimePNG: meta.SyntaxPNG,
+ mimeWEBP: meta.SyntaxWebp,
+ "text/html": meta.SyntaxHTML,
+ "text/markdown": meta.SyntaxMarkdown,
+ "text/plain": meta.SyntaxText,
+
+ // Additional syntaxes
+ "application/pdf": "pdf",
+ "text/javascript": "js",
+}
+
+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 web/content/content_test.go
Index: web/content/content_test.go
==================================================================
--- web/content/content_test.go
+++ web/content/content_test.go
@@ -0,0 +1,45 @@
+//-----------------------------------------------------------------------------
+// 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/parser"
+ "zettelstore.de/z/web/content"
+
+ _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser.
+ _ "zettelstore.de/z/parser/draw" // Allow to use draw parser.
+ _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser.
+ _ "zettelstore.de/z/parser/none" // Allow to use none parser.
+ _ "zettelstore.de/z/parser/plain" // Allow to use plain parser.
+ _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
+)
+
+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
+ }
+ }
+}
Index: web/server/impl/http.go
==================================================================
--- web/server/impl/http.go
+++ web/server/impl/http.go
@@ -1,16 +1,18 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2021 Detlef Stern
+// Copyright (c) 2020-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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 impl provides the Zettelstore web service.
package impl
import (
"context"
"net"
@@ -27,11 +29,10 @@
)
// httpServer is a HTTP server.
type httpServer struct {
http.Server
- waitStop chan struct{}
}
// initializeHTTPServer creates a new HTTP server object.
func (srv *httpServer) initializeHTTPServer(addr string, handler http.Handler) {
if addr == "" {
@@ -44,11 +45,10 @@
// See: https://blog.cloudflare.com/exposing-go-on-the-internet/
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
}
- srv.waitStop = make(chan struct{})
}
// SetDebug enables debugging goroutines that are started by the server.
// Basically, just the timeout values are reset. This method should be called
// before running the server.
Index: web/server/impl/impl.go
==================================================================
--- web/server/impl/impl.go
+++ web/server/impl/impl.go
@@ -1,13 +1,16 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2021-2022 Detlef Stern
+// 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 the Zettelstore web service.
package impl
@@ -14,29 +17,31 @@
import (
"context"
"net/http"
"time"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/auth"
- "zettelstore.de/z/domain/meta"
"zettelstore.de/z/logger"
"zettelstore.de/z/web/server"
+ "zettelstore.de/z/zettel/meta"
)
type myServer struct {
log *logger.Logger
+ baseURL string
server httpServer
router httpRouter
persistentCookie bool
secureCookie bool
}
// New creates a new web server.
-func New(log *logger.Logger, listenAddr, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server {
+func New(log *logger.Logger, listenAddr, baseURL, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server {
srv := myServer{
log: log,
+ baseURL: baseURL,
persistentCookie: persistentCookie,
secureCookie: secureCookie,
}
srv.router.initializeRouter(log, urlPrefix, maxRequestSize, auth)
srv.server.initializeHTTPServer(listenAddr, &srv.router)
@@ -53,21 +58,19 @@
srv.router.addZettelRoute(key, method, handler)
}
func (srv *myServer) SetUserRetriever(ur server.UserRetriever) {
srv.router.ur = ur
}
-func (srv *myServer) GetUser(ctx context.Context) *meta.Meta {
- if data := srv.GetAuthData(ctx); data != nil {
- return data.User
- }
- return nil
+
+func (srv *myServer) GetURLPrefix() string {
+ return srv.router.urlPrefix
}
func (srv *myServer) NewURLBuilder(key byte) *api.URLBuilder {
return api.NewURLBuilder(srv.GetURLPrefix(), key)
}
-func (srv *myServer) GetURLPrefix() string {
- return srv.router.urlPrefix
+func (srv *myServer) NewURLBuilderAbs(key byte) *api.URLBuilder {
+ return api.NewURLBuilder(srv.baseURL, key)
}
const sessionName = "zsession"
func (srv *myServer) SetToken(w http.ResponseWriter, token []byte, d time.Duration) {
@@ -90,40 +93,27 @@
}
}
// ClearToken invalidates the session cookie by sending an empty one.
func (srv *myServer) ClearToken(ctx context.Context, w http.ResponseWriter) context.Context {
- if authData := srv.GetAuthData(ctx); authData == nil {
+ if authData := server.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)
}
-// GetAuthData returns the full authentication data from the context.
-func (*myServer) GetAuthData(ctx context.Context) *server.AuthData {
- data, ok := ctx.Value(ctxKeySession).(*server.AuthData)
- if ok {
- return data
- }
- return nil
-}
-
-type ctxKeyTypeSession struct{}
-
-var ctxKeySession ctxKeyTypeSession
-
func updateContext(ctx context.Context, user *meta.Meta, data *auth.TokenData) context.Context {
if data == nil {
- return context.WithValue(ctx, ctxKeySession, &server.AuthData{User: user})
+ return context.WithValue(ctx, server.CtxKeySession, &server.AuthData{User: user})
}
return context.WithValue(
ctx,
- ctxKeySession,
+ server.CtxKeySession,
&server.AuthData{
User: user,
Token: data.Token,
Now: data.Now,
Issued: data.Issued,
Index: web/server/impl/router.go
==================================================================
--- web/server/impl/router.go
+++ web/server/impl/router.go
@@ -1,25 +1,27 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2022 Detlef Stern
+// 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 impl provides the Zettelstore web service.
package impl
import (
"io"
"net/http"
"regexp"
"strings"
- "zettelstore.de/c/api"
+ "t73f.de/r/zsc/api"
"zettelstore.de/z/auth"
"zettelstore.de/z/kernel"
"zettelstore.de/z/logger"
"zettelstore.de/z/web/server"
)
@@ -107,13 +109,13 @@
}
func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Something may panic. Ensure a kernel log.
defer func() {
- if reco := recover(); reco != nil {
+ if ri := recover(); ri != nil {
rt.log.Error().Str("Method", r.Method).Str("URL", r.URL.String()).HTTPIP(r).Msg("Recover context")
- kernel.Main.LogRecover("Web", reco)
+ kernel.Main.LogRecover("Web", ri)
}
}()
var withDebug bool
if msg := rt.log.Debug(); msg.Enabled() {
@@ -172,30 +174,30 @@
func (rt *httpRouter) addUserContext(r *http.Request) *http.Request {
if rt.ur == nil {
// No auth needed
return r
}
- k := auth.KindJSON
+ k := auth.KindAPI
t := getHeaderToken(r)
if len(t) == 0 {
rt.log.Debug().Msg("no jwt token found") // IP already logged: ServeHTTP
- k = auth.KindHTML
+ 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.Sense().Err(err).HTTPIP(r).Msg("invalid auth token")
+ 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.Sense().Zid(tokenData.Zid).Str("ident", tokenData.Ident).Err(err).HTTPIP(r).Msg("auth user not found")
+ 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))
}
Index: web/server/server.go
==================================================================
--- web/server/server.go
+++ web/server/server.go
@@ -1,13 +1,16 @@
//-----------------------------------------------------------------------------
-// Copyright (c) 2020-2021 Detlef Stern
+// Copyright (c) 2020-present Detlef Stern
//
-// This file is part of zettelstore.
+// 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
@@ -14,13 +17,13 @@
import (
"context"
"net/http"
"time"
- "zettelstore.de/c/api"
- "zettelstore.de/z/domain/id"
- "zettelstore.de/z/domain/meta"
+ "t73f.de/r/zsc/api"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/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)
@@ -50,22 +53,20 @@
// 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 {
- GetUser(context.Context) *meta.Meta
+ // 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
-
- // GetAuthData returns the full authentication data from the context.
- GetAuthData(ctx context.Context) *AuthData
}
// AuthData stores all relevant authentication data for a context.
type AuthData struct {
User *meta.Meta
@@ -72,10 +73,35 @@
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
Index: www/build.md
==================================================================
--- www/build.md
+++ www/build.md
@@ -1,57 +1,94 @@
-# How to build the Zettelstore
+# How to build Zettelstore
## Prerequisites
You must install the following software:
-* A current, supported [release of Go](https://golang.org/doc/devel/release.html),
+* A current, supported [release of Go](https://go.dev/doc/devel/release),
* [staticcheck](https://staticcheck.io/),
+* [shadow](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow),
+* [unparam](https://mvdan.cc/unparam),
+* [govulncheck](https://golang.org/x/vuln/cmd/govulncheck),
* [Fossil](https://fossil-scm.org/),
* [Git](https://git-scm.org) (so that Go can download some dependencies).
+See folder `docs/development` (a zettel box) for details.
+
## Clone the repository
-Most of this is covered by the excellent Fossil [documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki).
+Most of this is covered by the excellent Fossil
+[documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki).
1. Create a directory to store your Fossil repositories.
- Let's assume, you have created $HOME/fossils .
+ Let's assume, you have created `$HOME/fossils`.
1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossils/zettelstore.fossil`.
1. Create a working directory.
- Let's assume, you have created $HOME/zettelstore .
+ Let's assume, you have created `$HOME/zettelstore`.
1. Change into this directory: `cd $HOME/zettelstore`.
1. Open development: `fossil open $HOME/fossils/zettelstore.fossil`.
-(If you are not able to use Fossil, you could try the GitHub mirror
-.)
-
-## The build tool
-In directory tools there is a Go file called build.go .
-It automates most aspects, (hopefully) platform-independent.
-
-The script is called as:
+## Tools to build, test, and manage
+In the directory `tools` there are some Go files to automate most aspects of
+building and testing, (hopefully) platform-independent.
+
+The build script is called as:
```
-go run tools/build.go [-v] COMMAND
+go run tools/build/build.go [-v] COMMAND
```
The flag `-v` enables the verbose mode.
It outputs all commands called by the tool.
Some important `COMMAND`s are:
* `build`: builds the software with correct version information and puts it
- into a freshly created directory bin .
+ into a freshly created directory `bin`.
* `check`: checks the current state of the working directory to be ready for
release (or commit).
-* `clean`: removes the build directories and cleans the Go cache.
* `version`: prints the current version information.
Therefore, the easiest way to build your own version of the Zettelstore
software is to execute the command
```
-go run tools/build.go build
+go run tools/build/build.go build
```
In case of errors, please send the output of the verbose execution:
```
-go run tools/build.go -v build
+go run tools/build/build.go -v build
```
+
+Other tools are:
+
+* `go run tools/clean/clean.go` cleans your Go development worspace.
+* `go run tools/check/check.go` executes all linters and unit tests.
+ If you add the option `-r` linters are more strict, to be used for a
+ release version.
+* `go run tools/devtools/devtools.go` install all needed software (see above).
+* `go run tools/htmllint/htmllint.go [URL]` checks all generated HTML of a
+ Zettelstore accessible at the given URL (default: http://localhost:23123).
+* `go run tools/testapi/testapi.go` tests the API against a running
+ Zettelstore, which is started automatically.
+
+## A note on the use of Fossil
+Zettelstore is managed by the Fossil version control system. Fossil is an
+alternative to the ubiquitous Git version control system. However, Go seems to
+prefer Git and popular platforms that just support Git.
+
+Some dependencies of Zettelstore, namely [Zettelstore
+client](https://t73f.de/r/zsc) and [Sx](https://t73f.de/r/sx), are also
+managed by Fossil. Depending on your development setup, some error messages
+might occur.
+
+If the error message mentions an environment variable called `GOVCS` you should
+set it to the value `GOVCS=zettelstore.de:fossil` (alternatively more generous
+to `GOVCS=*:all`). Since the Go build system is coupled with Git and some
+special platforms, you allow ot to download a Fossil repository from the host
+`zettelstore.de`. The build tool set `GOVCS` to the right value, but you may
+use other `go` commands that try to download a Fossil repository.
+
+On some operating systems, namely Termux on Android, an error message might
+state that an user cannot be determined (`cannot determine user`). In this
+case, Fossil is allowed to download the repository, but cannot associate it
+with an user name. Set the environment variable `USER` to any user name, like:
+`USER=nobody go run tools/build.go build`.
Index: www/changes.wiki
==================================================================
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,11 +1,542 @@
Change Log
-
-Changes for Version 0.7.0 (pending)
+
+Changes for Version 0.18.0 (pending)
+ * Remove Sx macro defunconst
. Use defun
instead.
+ (breaking: webui)
+ * Update Sx prelude: make macros more robust / more general. This might
+ break your code in the future.
+ (minor: webui)
+ * Add computed zettel “Zettelstore Memory” with zettel
+ identifier 00000000000008
. It shows some statistics about
+ memory usage.
+ (minor: webui)
+ * Zettelstore client is now Go package t73f.de/r/zsc.
+ (minor)
+
+
+Changes for Version 0.17.0 (2024-03-04)
+ * Context search operates only on explicit references. Add the directive
+ FULL
to follow zettel tags additionally.
+ (breaking)
+ * Context cost calculation has been changed. Prepare to retrieve different
+ result.
+ (breaking)
+ * Remove metadata type WordSet. It was never implemented completely, and
+ nobody complained about this.
+ (breaking)
+ * Remove logging level “sense”, “warn”,
+ “fatal”, and “panic”.
+ (breaking)
+ * Add query action REDIRECT
which redirects to zettel that is
+ the first in the query result list.
+ (minor: api, webui)
+ * Add link to CONTEXT FULL
in the zettel info page.
+ (minor: webui)
+ * When generating HTML code to query set based metadata (esp. tags), also
+ generate a query that matches all values.
+ (minor: webui)
+ * Show all metadata with key ending “-url” on zettel view.
+ (minor: webui)
+ * Make WebUI form elements a little bit more accessible by using HTML
+ search
tag and inputmode
attribute.
+ (minor: webui)
+ * Add UI action for role zettel, similar to tag zettel. Obviously forgotten
+ in release 0.16.0, but thanks to the bug fix v0.16.1 detected.
+ (minor: webui)
+ * If an action, which is written in uppercase letters, results in an empty
+ list, the list of selected zettel is returned instead. This allows some
+ backward compatibility if a new action is introduced.
+ (minor)
+ * Only when query list is not empty, allow to show data and plain encoding,
+ an optionally show the “Save As Zettel” button.
+ (minor: webui)
+ * If query list is greater than three elements, show the number of elements
+ at bottom (before other encodings).
+ (minor: webui)
+ * Zettel with syntax “sxn” are pretty-printed during evaluation.
+ This allows to retrieve parsed zettel content, which checked for syntax,
+ but is not pretty-printed.
+ (minor)
+ * Some smaller bug fixes and improvements, to the software and to the
+ documentation.
+
+
+Changes for Version 0.16.1 (2023-12-28)
+ * Fix some Sxn definitions to allow role-based UI customizations.
+ (minor: webui)
+
+Changes for Version 0.16.0 (2023-11-30)
+ * Sx function define
is removed, as announced for version
+ 0.15.0. Use defvar
(to define variables) or
+ defun
(to define functions) instead. In addition
+ defunconst
defines a constant function, which ensures a fixed
+ binding of its name to its function body (performance optimization).
+ (breaking: webui)
+ * Allow to determine a role zettel for a given role.
+ (major: api, webui)
+ * Present user the option to create a (missing) role zettel (in list view).
+ Results in a new predefined zettel with identifier 00000000090004, which
+ is a template for new role zettel.
+ (minor: webui)
+ * Timestamp values can be abbrevated by omitting most of its components.
+ Previously, such values that are not in the format YYYYMMDDhhmmss were
+ ignored. Now the following formats are also allowed: YYYY, YYYYMM,
+ YYYYMMDD, YYYYMMDDhh, YYYYMMDDhhmm. Querying and sorting work accordingly.
+ Previously, only a sequences of zeroes were appended, resulting in illegal
+ timestamps, e.g. for YYYY or YYYYMM.
+ (minor)
+ * SHTML encoder fixed w.r.t inline quoting. Previously, an <q> tag was
+ used, which is inappropriate. Restored smart quotes from version 0.3, but
+ with new SxHTML infrastructure. This affect the html encoder and the WebUI
+ too. Now, an empty quote should not result in a warning by HTML linters.
+ (minor: api, webui)
+ * Add new zettelmarkup inline formatting: ##Text##
will mark /
+ highlight the given Text. It is typically used to highlight some text,
+ which is important for you, but not for the original author. When rendered
+ as HTML, the <mark> tag is used.
+ (minor: zettelmarkup)
+ * Add configuration keys to show, not to show, or show the closed list of
+ referencing zettel in the web user interface. You can set these
+ configurations system-wide, per user, or per zettel. Often it is used to
+ ensure a “clean” home zettel. Affects the list of incoming
+ / back links, folge zettel, subordinate zettel, and successor zettel.
+ (minor: webui)
+ * Some smaller bug fixes and improvements, to the software and to the
+ documentation.
+
+
+Changes for Version 0.15.0 (2023-10-26)
+ * Sx function define
is now deprecated. It will be removed in
+ version 0.16. Use defvar
or defun
instead.
+ Otherwise the WebUI will not work in version 0.16.
+ (major: webui, deprecated)
+ * Zettel can be re-indexed via WebUI or API query action
+ REINDEX
. The info page of a zettel contains a link to
+ re-index the zettel. In a query transclusion, this action is ignored.
+ (major: api, webui).
+ * Allow to determine a tag zettel for a given tag.
+ (major: api, webui)
+ * Present user the option to create a (missing) tag zettel (in list view).
+ Results in a new predefined zettel with identifier 00000000090003, which
+ is a template for new tag zettel.
+ (minor: webui)
+ * ZIP file with manual now contains a zettel 00001000000000 that contains
+ its build date (metadata key created
) and version (in the
+ zettel content)
+ (minor)
+ * If an error page cannot be created due to template errors (or similar), a
+ plain text error page is delivered instead. It shows the original error
+ and the error that occured durng rendering the original error page.
+ (minor: webui)
+ * Some smaller bug fixes and improvements, to the software and to the
+ documentation.
+
+
+Changes for Version 0.14.0 (2023-09-22)
+ * Remove support for JSON. This was marked deprecated in version 0.12.0. Use
+ the data
encoding instead, a form of symbolic expressions.
+ (breaking: api; minor: webui)
+ * Remove deprecated syntax for a context list: CONTEXT zid
. Use
+ zid CONTEXT
instead. It was deprecated in version 0.13.0.
+ (breaking: api, webui, zettelmarkup)
+ * Replace CSS-role-map mechanism with a more general Sx-based one: user
+ specific code may generates parts of resulting HTML document.
+ (breaking: webui)
+ * Allow meta-tags, i.e. zettel for a specific tag. Meta-tags have the tag
+ name as a title and specify the role "tag".
+ (major: webui)
+ * Allow to load sx code from multiple zettel; dependencies are specified
+ using precursor
metadata.
+ (major: webui)
+ * Allow sx code to change WebUI for zettel with specified role.
+ (major: webui)
+ * Some minor usability improvements.
+ (minor: webui)
+ * Some smaller bug fixes and improvements, to the software and to the
+ documentation.
+
+
+Changes for Version 0.13.0 (2023-08-07)
+ * There are for new search operators: less, not less, greater, not greater.
+ These use the same syntax as the operators prefix, not prefix, suffix, not
+ suffix. The latter are now denoted as [
, ![
,
+ ]
, and !]
. The first may operate numerically for
+ metadata like numbers, timestamps, and zettel identifier. They are not
+ supported for full-text search.
+ (breaking: api, webui)
+ * The API endpoint /o/{ID}
(order of zettel ID) is no longer
+ available. Please use the query expression {ID} ITEMS
+ instead.
+ (breaking: api)
+ * The API endpoint /u/{ID}
(unlinked references of zettel ID)
+ is no longer available. Please use the query expression {ID}
+ UNLINKED
instead.
+ (breaking: api)
+ * All API endpoints allow to encode zettel data with the data
+ encodings, incl. creating, updating, retrieving, and querying zettel.
+ (major: api)
+ * Change syntax for context query to zid ... CONTEXT
. This will
+ allow to add more directives that operate on zettel identifier. Old syntax
+ CONTEXT zid
will be removed in 0.14.
+ (major, deprecated)
+ * Add query directive ITEMS
that will produce a list of
+ metadata of all zettel that are referenced by the originating zettel in
+ a top-level list. It replaces the API endpoint /o/{ID}
(and
+ makes it more useful).
+ (major: api, webui)
+ * Add query directive UNLINKED
that will produce a list of
+ metadata of all zettel that are mentioning the originating zettel in
+ a top-level, but do not mention them. It replaces the API endpoint
+ /u/{ID}
(and makes it more useful).
+ (major: api, webui)
+ * Add query directive IDENT
to distinguish a search for
+ a zettel identifier (“{ID}”), that will list all metadata of
+ zettel containing that zettel identifier, and a request to just list the
+ metadata of given zettel (“{ID} IDENT”). The latter could be
+ filtered further.
+ (minor: api, webui)
+ * Add support for metadata key folge-role
.
+ (minor)
+ * Allow to create a child from a given zettel.
+ (minor: webui)
+ * Make zettel entry/edit form a little friendlier: auto-prepend missing '#'
+ to tags; ensure that role and syntax receive just a word.
+ (minor: webui)
+ * Use a zettel that defines builtins for evaluating WebUI templates.
+ (minor: webui)
+ * Add links to retrieve result of a query in other formats.
+ (minor: webui)
+ * Always log the found configuration file.
+ (minor: server)
+ * The use of the json
zettel encoding is deprecated (since
+ version 0.12.0). Support for this encoding will be removed in version
+ 0.14.0. Please use the new data
encoding instead.
+ (deprecated: api)
+ * Some smaller bug fixes and improvements, to the software and to the
+ documentation.
+
+
+Changes for Version 0.12.0 (2023-06-05)
+ * Syntax of templates for the web user interface are changed from Mustache
+ to Sxn (S-Expressions). Mustache is no longer supported, nowhere in the
+ software. Mustache was marked deprecated in version 0.11.0. If you
+ modified the template zettel, you must adapt to the new syntax.
+ (breaking: webui)
+ * Query expression is allowed to search for the "context" of a zettel.
+ Previously, this was a separate call, without adding a search expression
+ / action expression.
+ (breaking)
+ * "sexpr" encoding is renamed to "sz" encoding. This will affect mostly the
+ API. Additionally, all string "sexpr" are renamed to "sz" also. "Sz" is
+ the short form for "symbolic expression for zettel", similar to "shtml"
+ that is the short form for "symbolic expression for HTML".
+ (breaking)
+ * Render footer zettel on all WebUI pages.
+ (fix: webui)
+ * Query search operator "=" now compares for equality, ":" compares
+ depending on the value type.
+ (minor: api, webui)
+ * Search term PICK
now respects the original sort order. This
+ makes it more useful and orthogonal to RANDOM
and
+ LIMIT
. As a side effect, zettel lists retrieved via the API
+ are no longer sorted. In case you want a specific order, you must specify
+ it explicit.
+ (minor: api, webui)
+ * New metadata key expire
records a timestamp when a zettel
+ should be treated as, well, expired.
+ (minor)
+ * New metadata keys superior
and subordinate
+ (calculated from superior
) allow to specify a hierarchy
+ between zettel.
+ (minor)
+ * Metadata keys with suffix -date
and -time
are
+ treated as
+ timestamp values.
+ (minor)
+ * sexpr
zettel encoding is now documented in the manual.
+ (minor: manual)
+ * Build tool allows to install / update external Go tools needed to build
+ the software.
+ (minor)
+ * Show only useful metadata on WebUI, not the internal metadata.
+ (minor: webui)
+ * The use of the json
zettel encoding is deprecated. Support
+ for this encoding may be removed in future versions. Please use the new
+ data
encoding instead.
+ (deprecated: api)
+ * Some smaller bug fixes and improvements, to the software and to the
+ documentation.
+
+
+Changes for Version 0.11.2 (2023-04-16)
+ * Render footer zettel on all WebUI pages. Backported from 0.12.0. Many
+ thanks to HK for reporting it!
+ (fix: webui)
+
+Changes for Version 0.11.1 (2023-03-28)
+ * Make PICK
search term a little bit more deterministic so that
+ the “Save As Zettel” button produces the same list.
+ (fix: webui)
+
+Changes for Version 0.11.0 (2023-03-27)
+ * Remove ZJSON encoding. It was announced in version 0.10.0. Use Sexpr
+ encoding instead.
+ (breaking)
+ * Title of a zettel is no longer interpreted as Zettelmarkup text. Now it is
+ just a plain string, possibly empty. Therefore, no inline formatting (like
+ bold text), no links, no footnotes, no citations (the latter made
+ rendering the title often questionable, in some contexts). If you used
+ special entities, please use the unicode characters directly. However, as
+ a good practice, it is often the best to printable ASCII characters.
+ (breaking)
+ * Remove runtime configuration marker-external
. It was added in
+ version [#0_0_6|0.0.6] and updated in [#0_0_10|0.0.10]. If you want to
+ change the marker for an external URL, you could modify zettel
+ 00000000020001 (Zettelstore Base CSS) or zettel 00000000025001
+ (Zettelstore User CSS, preferred) by changing / adding a rule to add some
+ content after an external
tag.
+ (breaking: webui)
+ * Add SHTML encoding. This allows to ensure the quality of generated HTML
+ code. In addition, clients might use it, because it is easier to parse and
+ manipulate than ordinary HTML. In the future, HTML template zettel will
+ probably also use SHTML, deprecating the current Mustache syntax (which
+ was added in [#0_0_9|0.0.9]).
+ (major)
+ * Search term PICK n
, where n
is an integer value
+ greater zero, will pick randomly n
elements from the search
+ result list. Somehow similar (and faster) as RANDOM LIMIT n
,
+ but allows also later ordering of the resulting list.
+ (minor)
+ * Changed cost model for zettel context: a zettel with more
+ outgoing/incoming references has higher cost than a zettel with less
+ references. Also added support for traversing tags, with a similar cost
+ model. As an effect, zettel hubs (in many cases your home zettel) will
+ less likely add its references. Same for often used tags. The cost model
+ might change in some details in the future, but the idea of a penalty
+ applied to zettel / tags with many references will hold.
+ (minor)
+ * Some smaller bug fixes and improvements, to the software and to the
+ documentation.
+
+
+Changes for Version 0.10.1 (2023-01-30)
+ * Show button to save a query into a zettel only when the current user has
+ authorization to do it.
+ (fix: webui)
+
+Changes for Version 0.10.0 (2023-01-24)
+ * Remove support for endpoints /j, /m, /q, /p, /v
. Their
+ functions are merged into endpoint /z
. This was announced in
+ version 0.9.0. Please use only client library with at least version 0.10.0
+ too.
+ (breaking: api)
+ * Remove support for runtime configuration key footer-html
. Use
+ footer-zettel
instead. Deprecated in version 0.9.0.
+ (breaking: webui)
+ * Save a query into a zettel to freeze it.
+ (major: webui)
+ * Allow to show all used metadata keys, linked with their occurrences and
+ their values.
+ (minor: webui)
+ * Mark ZJSON encoding as deprecated for v0.11.0. Please use Sexpr encoding
+ instead.
+ (deprecated)
+ * Some smaller bug fixes and improvements, to the software and to the
+ documentation.
+
+
+Changes for Version 0.9.0 (2022-12-12)
+ * Remove support syntax pikchr
. Although it was a nice idea to
+ include it into Zettelstore, the implementation is too brittle (w.r.t. the
+ expected long lifetime of Zettelstore). There should be other ways to
+ support SVG front-ends.
+ (breaking)
+ * Allow to upload content when creating / updating a zettel.
+ (major: webui)
+ * Add syntax “draw” (again)
+ (minor: zettelmarkup)
+ * Allow to encode zettel in Markdown. Please note: not every aspect of
+ a zettel can be encoded in Markdown. Those aspects will be ignored.
+ (minor: api)
+ * Enhance zettel context by raising the importance of folge zettel (and
+ similar).
+ (minor: api, webui)
+ * Interpret zettel files with extension .webp
as an binary
+ image file format.
+ (minor)
+ * Allow to specify service specific log level via statup configuration and
+ via command line.
+ (minor)
+ * Allow to specify a zettel to serve footer content via runtime
+ comfiguration footer-zettel
. Can be overwritten by user
+ zettel.
+ (minor: webui)
+ * Footer data is automatically separated by a thematic break / horizontal
+ rule. If you do not like it, you have to update the base template.
+ (minor: webui)
+ * Allow to set runtime configuration home-zettel
in the user
+ zettel to make it user-specific.
+ (minor: webui)
+ * Serve favicon.ico from the asset directory.
+ (minor: webui)
+ * Zettelmarkup cheat sheet
+ (minor: manual)
+ * Runtime configuration key footer-html
will be removed in
+ Version 0.10.0. Please use footer-zettel
instead.
+ (deprecated: webui)
+ * In the next version 0.10.0, the API endpoints for a zettel
+ (/j
, /p
, /v
) will be merged with
+ endpoint /z
. Basically, the previous endpoint will be
+ refactored as query parameter of endpoint /z
. To reduce
+ errors, there will be no version, where the previous endpoint are still
+ available and the new funnctionality is still there. This is a warning to
+ prepare for some breaking changes in v0.10.0. This also affects the API
+ client implementation.
+ (warning: api)
+ * Some smaller bug fixes and improvements, to the software and to the
+ documentation.
+
+
+Changes for Version 0.8.0 (2022-10-20)
+ * Remove support for tags within zettel content. Removes also property
+ metadata keys all-tags
and computed-tags
.
+ Deprecated in version 0.7.0.
+ (breaking: zettelmarkup, api, webui)
+ * Remove API endpoint /m
, which retrieve aggregated (tags,
+ roles) zettel identifier. Deprecated in version 0.7.0.
+ (breaking: api)
+ * Remove support for URL query parameter starting with an underscore.
+ Deprecated in version 0.7.0.
+ (breaking: api, webui)
+ * Ignore HTML content by default, and allow HTML gradually by setting
+ startup value insecure-html
.
+ (breaking: markup)
+ * Endpoint /q
returns list of full metadata, if no query action
+ is specified. A HTTP call GET /z
(retrieving metadata of all
+ or some zettel) is now an alias for GET /q
.
+ (major: api)
+ * Allow to create a zettel that acts as the new version of an existing
+ zettel. Useful if you want to have access to older, outdated content.
+ (minor: webui)
+ * Allow transclusion to reference local image via URL.
+ (minor: zettelmarkup, webui)
+ * Add categories in RSS feed, based on zettel tags.
+ (minor: api, webui)
+ * Add support for creating an Atom 1.0 feed using a query action.
+ (minor: api, webui)
+ * Ignore entities with code point that is not allowed in HTML.
+ (minor: zettelmarkup)
+ * Enhance distribution of tag sizes when show a tag cloud.
+ (minor: webui)
+ * Warn user if zettelstore listens non-locally, but no authentication is
+ enabled.
+ (minor: server)
+ * Fix error that a manual zettel deletion was not always detected.
+ (bug: dirbox)
+ * Some smaller bug fixes and improvements, to the software and to the
+ documentation.
+
+
+Changes for Version 0.7.1 (2022-09-18)
+ * Produce a RSS feed compatible to Miniflux.
+ (minor)
+ * Make sure to always produce a pubdata in RSS feed.
+ (bug)
+ * Prefix search for data that looks like a zettel identifier may end with a
+ 0
.
+ (bug)
+ * Fix glitch on manual zettel.
+ (bug)
+
+Changes for Version 0.7.0 (2022-09-17)
+ * Removes support for URL query parameter to search for metadata values,
+ sorting, offset, and limit a zettel list. Deprecated in version 0.6.0
+ (breaking: api, webui)
+ * Allow to search for the existence / non-existence of a metadata key with
+ the "?" operator: key?
and key!?
. Previously,
+ the ":" operator was used for this by specifying an empty search value.
+ Now you can use the ":" operator to find empty / non-empty metadata
+ values. If you specify a search operator for metadata, the specified key
+ is assumed to exist.
+ (breaking: api, webui)
+ * Rename “search expression” into “query
+ expressions”. Similar, the reference prefix search:
to
+ specify a query link or a query transclusion is renamed to
+ query:
+ (breaking: zettelmarkup)
+ * Rename query parameter for query expression from _s
to
+ q
.
+ (breaking: api, webui)
+ * Cleanup names for HTTP query parameters in WebUI. Update your bookmarks
+ if you used them. (For API: see below)
+ (breaking: webui)
+ * Allow search terms to be OR-ed. This allows to specify any search
+ expression in disjunctive normal form. Therefore, the NEGATE term is not
+ needed any more.
+ (breaking: api, webui)
+ * Replace runtime configuration default-lang
with
+ lang
. Additionally, lang
set at the zettel of
+ the current user, will provide a default value for the current user,
+ overwriting the global default value.
+ (breaking)
+ * Add new syntax pikchr
, a markup language for diagrams in
+ technical documentation.
+ (major)
+ * Add endpoint /q
to query the zettelstore and aggregate
+ resulting values. This is done by extending the query syntax.
+ (major: api)
+ * Add support for query actions. Actions may aggregate w.r.t. some metadata
+ keys, or produce an RSS feed.
+ (major: api, webui)
+ * Query results can be ordered for more than one metadata key. Ordering by
+ zettel identifier is an implicit last order expression to produce stable
+ results.
+ (minor: api, webui)
+ * Add support for an asset directory, accessible via URL prefix
+ /assests/
.
+ (minor: server)
+ * Add support for metadata key created
, a timestamp when the
+ zettel was created. Since key published
is now either
+ created
or modified
, it will now always contains
+ a valid time stamp.
+ (minor)
+ * Add support for metadata key author
. It will be displayed on
+ a zettel, if set.
+ (minor: webui)
+ * Remove CSS for lists. The browsers default value for
+ padding-left
will be used.
+ (minor: webui)
+ * Removed templates for rendering roles and tags lists. This is now done by
+ query actions.
+ (minor: webui)
+ * Tags within zettel content are deprecated in version 0.8. This affects the
+ computed metadata keys content-tags
and
+ all-tags
. They will be removed. The number sign of a content
+ tag introduces unintended tags, esp. in the english language; content tags
+ may occur within links → links within links, when rendered as HTML;
+ content tags may occur in the title of a zettel; naming of content tags,
+ zettel tags, and their union is confusing for many. Migration: use zettel
+ tags or replace content tag with a search.
+ (deprecated: zettelmarkup)
+ * Cleanup names for HTTP query parameter for API calls. Essentially,
+ underscore characters in front are removed. Please use new names, old
+ names will be deprecated in version 0.8.
+ (deprecated: api)
+ * Some smaller bug fixes and improvements, to the software and to the
+ documentation.
+
+
+Changes for Version 0.6.2 (2022-08-22)
+ * Recognize renaming of zettel file external to Zettelstore.
+ (bug)
-
+Changes for Version 0.6.1 (2022-08-22)
+ * Ignore empty tags when reading metadata.
+ (bug)
+
Changes for Version 0.6.0 (2022-08-11)
* Translating of "..." into horizontal ellipsis is no longer supported. Use
… instead.
(breaking: zettelmarkup)
* Allow to specify search expressions, which allow to specify search
@@ -31,14 +562,14 @@
ordering, an offset, and a limit for the resulting list, will be removed
in version 0.7. Replace these with the more useable search expressions.
Please be aware that the = search operator is also deprecated. It was only
introduced to help the migration.
(deprecated: api, webui)
- * Some smaller bug fixes and inprovements, to the software and to the
+ * Some smaller bug fixes and improvements, to the software and to the
documentation.
-
+
Changes for Version 0.5.1 (2022-08-02)
* Log missing authentication tokens in debug level (was: sense level)
(major)
* Allow to use empty metadata values of string and zmk types.
(minor)
@@ -56,14 +587,14 @@
(breaking)
* “Sexpr” encoding replaces “Native” encoding. Sexpr
encoding is much easier to parse, compared with native and ZJSON encoding.
In most cases it is smaller than ZJSON.
(breaking: api)
- * Endpoint /r is changed to /m?_key=role and returns now
- a map of role names to the list of zettel having this role. Endpoint
- /t is changed to /m?_key=tags . It already returned
- mapping described before.
+ * Endpoint /r
is changed to /m?_key=role
and
+ returns now a map of role names to the list of zettel having this role.
+ Endpoint /t
is changed to /m?_key=tags
. It
+ already returned mapping described before.
(breaking: api)
* Remove support for a default value for metadata key title, role, and
syntax. Title and role are now allowed to be empty, an empty syntax value
defaults to “plain”.
(breaking)
@@ -96,20 +627,20 @@
web server log messages.
(minor: web server)
* New startup configuration key max-request-size to limit a web
request body to prevent client sending too large requests.
(minor: web server)
- * Many smaller bug fixes and inprovements, to the software and to the
+ * Many smaller bug fixes and improvements, to the software and to the
documentation.
-
+
Changes for Version 0.4 (2022-03-08)
* Encoding “djson” renamed to “zjson” (zettel
json ).
(breaking: api; minor: webui)
- * Remove inline quotation syntax <<...<< . Now,
- ""..."" generates the equivalent code.
+ * Remove inline quotation syntax <<...<<
. Now,
+ ""...""
generates the equivalent code.
Typographical quotes are generated by the browser, not by Zettelstore.
(breaking: Zettelmarkup)
* Remove inline formatting for monospace. Its syntax is now used by the
similar syntax element of literal computer input. Monospace was just
a visual element with no semantic association. Now, the syntax
@@ -136,11 +667,11 @@
identifier.
(minor: api, webui)
* Change generated URLs for zettel-creation forms. If you have bookmarked
them, e.g. to create a new zettel, you should update.
(minor: webui)
- * Remove support for metadata key no-index to suppress indexing
+ * Remove support for metadata key no-index
to suppress indexing
selected zettel. It was introduced in v0.0.11 , but
disallows some future optimizations for searching zettel.
(minor: api, webui)
* Make some metadata-based searches a little bit faster by executing
a (in-memory-based) full-text search first. Now only those zettel are
@@ -152,16 +683,16 @@
to use it with public access.
(minor: box)
* Disallow to cache the authentication cookie. Will remove most unexpected
log-outs when using a mobile device.
(minor: webui)
- * Many smaller bug fixes and inprovements, to the software and to the
+ * Many smaller bug fixes and improvements, to the software and to the
documentation.
-
+
Changes for Version 0.3 (2022-02-09)
- * Zettel files with extension .meta are now treated as content
+ * Zettel files with extension .meta
are now treated as content
files. Previoulsy, they were interpreted as metadata files. The
interpretation as metadata files was deprecated in version 0.2.
(breaking: directory and file/zip box)
* Add syntax “draw” to produce some graphical representations.
(major)
@@ -174,56 +705,57 @@
access rights for the given zettel.
(minor: api)
* A previously duplicate file that is now useful (because another file was
deleted) is now logged as such.
(minor: directory and file/zip box)
- * Many smaller bug fixes and inprovements, to the software and to the
+ * Many smaller bug fixes and improvements, to the software and to the
documentation.
-
+
Changes for Version 0.2 (2022-01-19)
* v0.2.1 (2021-02-01) updates the license year in some documents
- * Remove support for ;;small text;; Zettelmarkup.
+ * Remove support for ;;small text;;
Zettelmarkup.
(breaking: Zettelmarkup)
* On macOS, the downloadable executable program is now called
“zettelstore”, as on all other Unix-like platforms.
(possibly breaking: macOS)
* External metadata (e.g. for zettel with file extension other than
- .zettel ) are stored in files without an extension. Metadata files
- with extension .meta are still recognized, but result in
- a warning message. In a future version (probably v0.3), .meta
- files will be treated as ordinary content files, possibly resulting in
- duplicate content. In other words: usage of .meta files for
- storing metadata is deprecated.
+ .zettel
) are stored in files without an extension. Metadata
+ files with extension .meta
are still recognized, but result
+ in a warning message. In a future version (probably v0.3),
+ .meta
files will be treated as ordinary content files,
+ possibly resulting in duplicate content. In other words: usage of
+ .meta
files for storing metadata is deprecated.
(possibly breaking: directory and file box)
* Show unlinked references in info page of each zettel. Unlinked references
are phrases within zettel content that might reference another zettel with
the same title as the phase.
(major: webui)
- * Add endpoint /u/{ID} to retrieve unlinked references.
+ * Add endpoint /u/{ID}
to retrieve unlinked references.
(major: api)
* Provide a logging facility.
Log messages are written to standard output. Messages with level
“information” are also written to a circular buffer (of length
8192) which can be retrieved via a computed zettel. There is a command
- line flag -l LEVEL to specify an application global logging level
- on startup (default: “information”). Logging level can also be
- changed via the administrator console, even for specific (sub-) services.
+ line flag -l LEVEL
to specify an application global logging
+ level on startup (default: “information”). Logging level can
+ also be changed via the administrator console, even for specific (sub-)
+ services.
(major)
* The internal handling of zettel files is rewritten. This allows less
reloads ands detects when the directory containing the zettel files is
removed. The API, WebUI, and the admin console allow to manually refresh
the internal state on demand.
(major: box, webui)
- * .zettel files with YAML header are now correctly written.
+ * .zettel
files with YAML header are now correctly written.
(bug)
* Selecting zettel based on their metadata allows the same syntax as
searching for zettel content. For example, you can list all zettel that
- have an identifier not ending with 00 by using the query
- id=!<00 .
+ have an identifier not ending with 00
by using the query
+ id=!<00
.
(minor: api, webui)
- * Remove support for //deprecated emphasized// Zettelmarkup.
+ * Remove support for //deprecated emphasized//
Zettelmarkup.
(minor: Zettelmarkup)
* Add options to profile the software. Profiling can be enabled at the
command line or via the administrator console.
(minor)
* Add computed zettel that lists all supported parser / recognized zettel
@@ -233,43 +765,43 @@
(minor: api)
* Renewing an API access token works even if authentication is not enabled.
This corresponds to the behaviour of optaining an access token.
(minor: api)
* If there is nothing to return, use HTTP status code 204, instead of 200 +
- Content-Length: 0 .
+ Content-Length: 0
.
(minor: api)
- * Metadata key duplicates stores the duplicate file names, instead
- of just a boolean value that there were duplicate file names.
+ * Metadata key duplicates
stores the duplicate file names,
+ instead of just a boolean value that there were duplicate file names.
(minor)
* Document autostarting Zettelstore on Windows, macOS, and Linux.
(minor)
- * Many smaller bug fixes and inprovements, to the software and to the
+ * Many smaller bug fixes and improvements, to the software and to the
documentation.
-
+
Changes for Version 0.1 (2021-11-11)
* v0.1.3 (2021-12-15) fixes a bug where the modification date could be set
when a new zettel is created.
* v0.1.2 (2021-11-18) fixes a bug when selecting zettel from a list when
more than one comparison is negated.
* v0.1.1 (2021-11-12) updates the documentation, mostly related to the
- deprecation of the // markup.
+ deprecation of the //
markup.
* Remove visual Zettelmarkup (italic, underline). Semantic Zettelmarkup
(emphasize, insert) is still allowed, but got a different syntax. The new
- syntax for inserted text is >>inserted>> ,
- while its previous syntax now denotes emphasized text :
- __emphasized__ . The previous syntax for emphasized text is now
- deprecated: //deprecated emphasized// . Starting with
- Version 0.2.0, the deprecated syntax will not be supported. The
- reason is the collision with URLs that also contain the characters
- // . The ZMK encoding of a zettel may help with the transition
- (/v/{ZettelID}?_part=zettel&_enc=zmk , on the Info page of
+ syntax for inserted text is
+ >>inserted>>
, while its previous syntax now
+ denotes emphasized text : __emphasized__
. The
+ previous syntax for emphasized text is now deprecated: //deprecated
+ emphasized//
. Starting with Version 0.2.0, the deprecated
+ syntax will not be supported. The reason is the collision with URLs that
+ also contain the characters //
. The ZMK encoding of a zettel
+ may help with the transition
+ (/v/{ZettelID}?_part=zettel&_enc=zmk
, on the Info page of
each zettel in the WebUI). Additionally, all deprecated uses of
- // will be rendered with a dashed box within the WebUI.
+ //
will be rendered with a dashed box within the WebUI.
(breaking: Zettelmarkup).
- * API client software is now a [https://zettelstore.de/client/|separate]
- project.
+ * API client software is now a separate project.
(breaking)
* Initial support for HTTP security headers (Content-Security-Policy,
Permissions-Policy, Referrer-Policy, X-Content-Type-Options,
X-Frame-Options). Header values are currently some constant values.
(possibly breaking: api, webui)
@@ -276,20 +808,21 @@
* Remove visual Zettelmarkup (bold, striketrough). Semantic Zettelmarkup
(strong, delete) is still allowed and replaces the visual elements
syntactically. The visual appearance should not change (depends on your
changes / additions to CSS zettel).
(possibly breaking: Zettelmarkup).
- * Add API endpoint POST /v to retrieve HTMl and text encoded
+ * Add API endpoint POST /v
to retrieve HTMl and text encoded
strings from given ZettelMarkup encoded values. This will be used to
render a HTML page from a given zettel: in many cases the title of
a zettel must be treated separately.
(minor: api)
- * Add API endpoint /m to retrieve only the metadata of a zettel.
+ * Add API endpoint /m
to retrieve only the metadata of
+ a zettel.
(minor: api)
- * New metadata value content-tags contains the tags that were given
- in the zettel content. To put it simply, all-tags = tags
- + content-tags .
+ * New metadata value content-tags
contains the tags that were
+ given in the zettel content. To put it simply, all-tags
+ = tags
+ content-tags
.
(minor)
* Calculating the context of a zettel stops at the home zettel.
(minor: api, webui)
* When renaming or deleting a zettel, a warning will be given, if other
zettel references the given zettel, or when “deleting” will
@@ -305,104 +838,105 @@
(minor: webui)
* Separate repository for [https://zettelstore.de/contrib/|contributed]
software. First entry is a software for creating a presentation by using
zettel.
(info)
- * Many smaller bug fixes and inprovements, to the software and to the
+ * Many smaller bug fixes and improvements, to the software and to the
documentation.
-
+
Changes for Version 0.0.15 (2021-09-17)
* Move again endpoint characters for authentication to make room for future
- features. WebUI authentication moves from /a to /i
- (login) and /i?logout (logout). API authentication moves from
- /v to /a. JSON-based basic zettel handling moves from
- /z to /j and /z/{ID} to /j/{ID} . Since
- the API client is updated too, this should not be a breaking change for
- most users.
+ features. WebUI authentication moves from /a
to
+ /i
(login) and /i?logout
(logout). API
+ authentication moves from /v
to /a. JSON-based
+ basic zettel handling moves from /z
to /j
and
+ /z/{ID}
to /j/{ID}
. Since the API client is
+ updated too, this should not be a breaking change for most users.
(minor: api, webui; possibly breaking)
- * Add API endpoint /v/{ID} to retrieve an evaluated zettel in
- various encodings. Mostly replaces endpoint /z/{ID} for other
+ * Add API endpoint /v/{ID}
to retrieve an evaluated zettel in
+ various encodings. Mostly replaces endpoint /z/{ID}
for other
encodings except “json” and “raw”. Endpoint
- /j/{ID} now only returns JSON data, endpoint /z/{ID} is
- used to retrieve plain zettel data (previously called “raw”).
- See documentation for details.
+ /j/{ID}
now only returns JSON data, endpoint
+ /z/{ID}
is used to retrieve plain zettel data (previously
+ called “raw”). See documentation for details.
(major: api; breaking)
* Metadata values of type tag set (the metadata with key
- tags is its most prominent example), are now compared in
+ tags
is its most prominent example), are now compared in
a case-insensitive manner. Tags that only differ in upper / lower case
character are now treated identical. This might break your workflow, if
you depend on case-sensitive comparison of tag values. Tag values are
translated to their lower case equivalent before comparing them and when
you edit a zettel through Zettelstore. If you just modify the zettel
files, your tag values remain unchanged.
(major; breaking)
- * Endpoint /z/{ID} allows the same methods as endpoint
- /j/{ID} : GET retrieves zettel (see above), PUT
- updates a zettel, DELETE deletes a zettel, MOVE renames
- a zettel. In addtion, POST /z will create a new zettel. When
- zettel data must be given, the format is plain text, with metadata
- separated from content by an empty line. See documentation for more
- details.
+ * Endpoint /z/{ID}
allows the same methods as endpoint
+ /j/{ID}
: GET
retrieves zettel (see above),
+ PUT
updates a zettel, DELETE
deletes a zettel,
+ MOVE
renames a zettel. In addtion, POST /z
will
+ create a new zettel. When zettel data must be given, the format is plain
+ text, with metadata separated from content by an empty line. See
+ documentation for more details.
(major: api (plus WebUI for some details))
* Allows to transclude / expand the content of another zettel into a target
zettel when the zettel is rendered. By using the syntax of embedding an
image (which is some kind of expansion too), the first top-level paragraph
of a zettel may be transcluded into the target zettel. Endless recursion
is checked, as well as a possible “transclusion bomb ”
(similar to a XML bomb). See manual for details.
(major: zettelmarkup)
- * The endpoint /z allows to list zettel in a simpler format than
- endpoint /j : one line per zettel, and only zettel identifier plus
- zettel title.
+ * The endpoint /z
allows to list zettel in a simpler format
+ than endpoint /j
: one line per zettel, and only zettel
+ identifier plus zettel title.
(minor: api)
* Folgezettel are now displayed with full title at the bottom of a page.
(minor: webui)
- * Add API endpoint /p/{ID} to retrieve a parsed, but not evaluated
- zettel in various encodings.
+ * Add API endpoint /p/{ID}
to retrieve a parsed, but not
+ evaluated zettel in various encodings.
(minor: api)
* Fix: do not list a shadowed zettel that matches the select criteria.
(minor)
- * Many smaller bug fixes and inprovements, to the software and to the
+ * Many smaller bug fixes and improvements, to the software and to the
documentation.
-
+
Changes for Version 0.0.14 (2021-07-23)
* Rename “place” into “box”. This also affects the
- configuration keys to specify boxes box-uriX (previously
- place-uri-X . Older changes documented here are renamed
- too.
+ configuration keys to specify boxes box-uriX
+ (previously place-uri-X
. Older changes documented
+ here are renamed too.
(breaking)
* Add API for creating, updating, renaming, and deleting zettel.
(major: api)
* Initial API client for Go.
(major: api)
* Remove support for paging of WebUI list. Runtime configuration key
- list-page-size is removed. If you still specify it, it will be
- ignored.
+ list-page-size
is removed. If you still specify it, it will
+ be ignored.
(major: webui)
- * Use endpoint /v for user authentication via API. Endpoint
- /a is now used for the web user interface only. Similar, endpoint
- /y (“zettel context”) is renamed to /x .
+ * Use endpoint /v
for user authentication via API. Endpoint
+ /a
is now used for the web user interface only. Similar,
+ endpoint /y
(“zettel context”) is renamed to
+ /x
.
(minor, possibly breaking)
* Type of used-defined metadata is determined by suffix of key:
- -number , -url , -zid will result the values to
- be interpreted as a number, an URL, or a zettel identifier.
+ -number
, -url
, -zid
will result the
+ values to be interpreted as a number, an URL, or a zettel identifier.
(minor, but possibly breaking if you already used a metadata key with
above suffixes, but as a string type)
- * New user-role “creator”, which is only allowed to
+ * New user-role
“creator”, which is only allowed to
create new zettel (except user zettel). This role may only read and update
public zettel or its own user zettel. Added to support future client
software (e.g. on a mobile device) that automatically creates new zettel
but, in case of a password loss, should not allow to read existing zettel.
(minor, possibly breaking, because new zettel template zettel must always
- prepend the string new- before metdata keys that should be
+ prepend the string new-
before metdata keys that should be
transferred to the new zettel)
- * New suported metadata key box-number , which gives an indication
- from which box the zettel was loaded.
+ * New suported metadata key box-number
, which gives an
+ indication from which box the zettel was loaded.
(minor)
- * New supported syntax html .
+ * New supported syntax html
.
(minor)
* New predefined zettel “User CSS” that can be used to redefine
some predefined CSS (without modifying the base CSS zettel).
(minor: webui)
* When a user moves a zettel file with additional characters into the box
@@ -409,21 +943,21 @@
directory, these characters are preserved when zettel is updated.
(bug)
* The phase “filtering a zettel list” is more precise
“selecting zettel”
(documentation)
- * Many smaller bug fixes and inprovements, to the software and to the
+ * Many smaller bug fixes and improvements, to the software and to the
documentation.
-
+
Changes for Version 0.0.13 (2021-06-01)
- * Startup configuration box-X -uri (where X is a
- number greater than zero) has been renamed to
- box-uri-X .
+ * Startup configuration box-X -uri
(where X is
+ a number greater than zero) has been renamed to
+ box-uri-X
.
(breaking)
- * Web server processes startup configuration url-prefix . There is
- no need for stripping the prefix by a front-end web server any more.
+ * Web server processes startup configuration url-prefix
. There
+ is no need for stripping the prefix by a front-end web server any more.
(breaking: webui, api)
* Administrator console (only optional accessible locally). Enable it only
on systems with a single user or with trusted users. It is disabled by
default.
(major: core)
@@ -430,12 +964,13 @@
* Remove visibility value “simple-expert” introduced in
[#0_0_8|version 0.0.8]. It was too complicated, esp. authorization. There
was a name collision with the “simple” directory box sub-type.
(major)
* For security reasons, HTML blocks are not encoded as HTML if they contain
- certain snippets, such as <script or <iframe .
- These may be caused by using CommonMark as a zettel syntax.
+ certain snippets, such as <script
or
+ <iframe
. These may be caused by using CommonMark as
+ a zettel syntax.
(major)
* Full-text search can be a prefix search or a search for equal words, in
addition to the search whether a word just contains word of the search
term.
(minor: api, webui)
@@ -450,17 +985,17 @@
* Local images that cannot be read (not found or no access rights) are
substituted with the new default image, a spinning emoji.
See [/file?name=box/constbox/emoji_spin.gif].
(minor: webui)
* Add zettelmarkup syntax for a table row that should be ignored:
- |% . This allows to paste output of the administrator console into
- a zettel.
+ |%
. This allows to paste output of the administrator console
+ into a zettel.
(minor: zmk)
- * Many smaller bug fixes and inprovements, to the software and to the
+ * Many smaller bug fixes and improvements, to the software and to the
documentation.
-
+
Changes for Version 0.0.12 (2021-04-16)
* Raise the per-process limit of open files on macOS to 1.048.576. This
allows most macOS users to use at least 500.000 zettel. That should be
enough for the near future.
(major)
@@ -468,18 +1003,18 @@
directory boxes. The original directory box type is now called "notify"
(the default value). There is a new type called "simple". This new type
does not notify Zettelstore when some of the underlying Zettel files
change.
(major)
- * Add new startup configuration default-dir-box-type , which gives
- the default value for specifying a directory box type. The default value
- is “notify”. On macOS, the default value may be changed
+ * Add new startup configuration default-dir-box-type
, which
+ gives the default value for specifying a directory box type. The default
+ value is “notify”. On macOS, the default value may be changed
“simple” if some errors occur while raising the per-process
limit of open files.
(minor)
-
+
Changes for Version 0.0.11 (2021-04-05)
* New box schema "file" allows to read zettel from a ZIP file.
A zettel collection can now be packaged and distributed easier.
(major: server)
* Non-restricted search is a full-text search. The search string will be
@@ -487,11 +1022,11 @@
or a number will be ignored for the search. It is sufficient if the words
to be searched are part of words inside a zettel, both content and
metadata.
(major: api, webui)
* A zettel can be excluded from being indexed (and excluded from being found
- in a search) if it contains the metadata no-index: true .
+ in a search) if it contains the metadata no-index: true
.
(minor: api, webui)
* Menu bar is shown when displaying error messages.
(minor: webui)
* When selecting zettel, it can be specified that a given value should
not match. Previously, only the whole select criteria could be
@@ -507,106 +1042,118 @@
* Selecting zettel depending on tag values can be both by comparing only the
prefix or the whole string. If a search value begins with '#', only zettel
with the exact tag will be returned. Otherwise a zettel will be returned
if the search string just matches the prefix of only one of its tags.
(minor: api, webui)
- * Many smaller bug fixes and inprovements, to the software and to the documentation.
+ * Many smaller bug fixes and improvements, to the software and to the
+ documentation.
A note for users of macOS: in the current release and with macOS's default
values, a zettel directory must not contain more than approx. 250 files. There
are three options to mitigate this limitation temporarily:
# You update the per-process limit of open files on macOS.
- # You setup a virtualization environment to run Zettelstore on Linux or Windows.
+ # You setup a virtualization environment to run Zettelstore on Linux or
+ Windows.
# You wait for version 0.0.12 which addresses this issue.
-
+
Changes for Version 0.0.10 (2021-02-26)
* Menu item “Home” now redirects to a home zettel.
- Its default identifier is 000100000000 .
- The identifier can be changed with configuration key home-zettel , which supersedes key start .
- The default home zettel contains some welcoming information for the new user.
+ Its default identifier is 000100000000
. The identifier can be
+ changed with configuration key home-zettel
, which supersedes
+ key start
. The default home zettel contains some welcoming
+ information for the new user.
(major: webui)
- * Show context of a zettel by following all backward and/or forward reference
- up to a defined depth and list the resulting zettel. Additionally, some zettel
- with similar tags as the initial zettel are also taken into account.
+ * Show context of a zettel by following all backward and/or forward
+ reference up to a defined depth and list the resulting zettel.
+ Additionally, some zettel with similar tags as the initial zettel are also
+ taken into account.
(major: api, webui)
- * A zettel that references other zettel within first-level list items, can act
- as a “table of contents” zettel.
- The API endpoint /o/{ID} allows to retrieve the referenced zettel in
- the same order as they occur in the zettel.
+ * A zettel that references other zettel within first-level list items, can
+ act as a “table of contents” zettel. The API endpoint
+ /o/{ID}
allows to retrieve the referenced zettel in the same
+ order as they occur in the zettel.
(major: api)
- * The zettel “New Menu” with identifier 00000000090000 contains
- a list of all zettel that should act as a template for new zettel.
- They are listed in the WebUIs ”New“ menu.
- This is an application of the previous item.
- It supersedes the usage of a role new-template introduced in [#0_0_6|version 0.0.6].
- Please update your zettel if you make use of the now deprecated feature.
+ * The zettel “New Menu” with identifier
+ 00000000090000
contains a list of all zettel that should act
+ as a template for new zettel. They are listed in the WebUIs
+ ”New“ menu. This is an application of the previous item. It
+ supersedes the usage of a role new-template
introduced in
+ [#0_0_6|version 0.0.6]. Please update your zettel if you make use of
+ the now deprecated feature.
(major: webui)
- * A reference that starts with two slash characters (“//
”)
- it will be interpreted relative to the value of url-prefix
.
- For example, if url-prefix
has the value /manual/
,
- the reference [[Zettel list|//h]]
will render as
- <a href="/manual/h">Zettel list</a>
. (minor: syntax)
+ * A reference that starts with two slash characters
+ (“//
”) it will be interpreted relative to the
+ value of url-prefix
. For example, if url-prefix
+ has the value /manual/
, the reference
+ [[Zettel list|//h]]
will render as <a
+ href="/manual/h">Zettel list</a>
.
+ (minor: syntax)
* Searching/selecting ignores the leading '#' character of tags.
(minor: api, webui)
- * When result of selecting or searching is presented, the query is written as the page heading.
+ * When result of selecting or searching is presented, the query is written
+ as the page heading.
(minor: webui)
- * A reference to a zettel that contains a URL fragment, will now be processed by the indexer.
+ * A reference to a zettel that contains a URL fragment, will now be
+ processed by the indexer.
(bug: server)
- * Runtime configuration key marker-external now defaults to
+ * Runtime configuration key marker-external
now defaults to
“➚” (“➚”). It is more beautiful
than the previous “↗︎”
(“↗︎”), which also needed the additional
- “︎” to disable the conversion to an emoji on iPadOS.
+ “︎” to disable the conversion to an emoji on
+ iPadOS.
(minor: webui)
- * A pre-build binary for macOS ARM64 (also known as Apple silicon) is available.
+ * A pre-build binary for macOS ARM64 (also known as Apple silicon) is
+ available.
(minor: infrastructure)
- * Many smaller bug fixes and inprovements, to the software and to the documentation.
+ * Many smaller bug fixes and improvements, to the software and to the
+ documentation.
-
+
Changes for Version 0.0.9 (2021-01-29)
This is the first version that is managed by [https://fossil-scm.org|Fossil]
instead of GitHub. To access older versions, use the Git repository under
[https://github.com/zettelstore/zettelstore-github|zettelstore-github].
Server / API
* (major) Support for property metadata.
- Metadata key published is the first example of such
+ Metadata key published
is the first example of such
a property.
* (major) A background activity (called indexer ) continuously
monitors zettel changes to establish the reverse direction of
found internal links. This affects the new metadata keys
- precursor and folge . A user specifies the
- precursor of a zettel and the indexer computes the property
+ precursor
and folge
. A user specifies
+ the precursor of a zettel and the indexer computes the property
metadata for
[https://forum.zettelkasten.de/discussion/996/definition-folgezettel|Folgezettel].
Metadata keys with type “Identifier” or
“IdentifierSet” that have no inverse key (like
- precursor and folge with add to the key
- forward that also collects all internal links within the
- content. The computed inverse is backward , which provides
- all backlinks. The key back is computed as the value of
- backward , but without forward links. Therefore,
- back is something like the list of “smart
- backlinks”.
+ precursor
and folge
with add to the key
+ forward
that also collects all internal links within
+ the content. The computed inverse is backward
, which
+ provides all backlinks. The key back
is computed as
+ the value of backward
, but without forward links.
+ Therefore, back
is something like the list of
+ “smart backlinks”.
* (minor) If Zettelstore is being stopped, an appropriate message is written
in the console log.
* (minor) New computed zettel with environmental data, the list of supported
meta data keys, and statistics about all configured zettel boxes.
Some other computed zettel got a new identifier (to make room for
other variant).
- * (minor) Remove zettel 00000000000004 , which contained the Go
+ * (minor) Remove zettel 00000000000004
, which contained the Go
version that produced the Zettelstore executable. It was too
specific to the current implementation. This information is now
- included in zettel 00000000000006 (Zettelstore
+ included in zettel 00000000000006
(Zettelstore
Environment Values ).
* (minor) Predefined templates for new zettel do not contain any value for
- attribute visibility any more.
+ attribute visibility
any more.
* (minor) Add a new metadata key type called “Zettelmarkup”.
It is a non-empty string, that will be formatted with
- Zettelmarkup. title and default-title have this
- type.
+ Zettelmarkup. title
and default-title
+ have this type.
* (major) Rename zettel syntax “meta” to “none”.
Please update the Zettelstore Runtime Configuration and all
other zettel that previously used the value “meta”.
Other zettel are typically user zettel, used for authentication.
However, there is no real harm, if you do not update these zettel.
@@ -613,12 +1160,13 @@
In this case, the metadata is just not presented when rendered.
Zettelstore will still work.
* (minor) Login will take at least 500 milliseconds to mitigate login
attacks. This affects both the API and the WebUI.
* (minor) Add a sort option “_random” to produce a zettel list
- in random order. _order / order are now an
- aliases for the query parameters _sort / sort .
+ in random order. _order
/ order
are now
+ an aliases for the query parameters _sort
+ / sort
.
WebUI
* (major) HTML template zettel for WebUI now use
[https://mustache.github.io/|Mustache] syntax instead of
previously used [https://golang.org/pkg/html/template/|Go
@@ -632,70 +1180,71 @@
header of a rendered zettel. If a zettel has real backlinks, they
are shown at the botton of the page (“Additional links to
this zettel”).
* (minor) All property metadata, even computed metadata is shown in the info
page of a zettel.
- * (minor) Rendering of metadata keys title and
- default-title in info page changed to a full HTML output
- for these Zettelmarkup encoded values.
+ * (minor) Rendering of metadata keys title
and
+ default-title
in info page changed to a full HTML
+ output for these Zettelmarkup encoded values.
* (minor) Always show the zettel identifier on the zettel detail view.
Previously, the identifier was not shown if the zettel was not
editable.
* (minor) Do not show computed metadata in edit forms anymore.
-
+
Changes for Version 0.0.8 (2020-12-23)
Server / API
- * (bug) Zettel files with extension .jpg and without metadata will
- get a syntax value “jpg”. The internal data
- structure got the same value internally, instead of
+ * (bug) Zettel files with extension .jpg
and without metadata
+ will get a syntax
value “jpg”. The internal
+ data structure got the same value internally, instead of
“jpeg”. This has been fixed for all possible alternative
syntax values.
- * (bug) If a file, e.g. an image file like 20201130190200.jpg , is
- added to the directory box, its metadata are just calculated from
+ * (bug) If a file, e.g. an image file like 20201130190200.jpg
,
+ is added to the directory box, its metadata are just calculated from
the information available. Updated metadata did not find its way
- into the zettel box, because the .meta file was not
+ into the zettel box, because the .meta
file was not
written.
- * (bug) If just the .meta file was deleted manually, the zettel was
- assumed to be missing. A workaround is to restart the software. If
- the .meta file is deleted, metadata is now calculated in
- the same way when the .meta file is non-existing at the
- start of the software.
+ * (bug) If just the .meta
file was deleted manually, the zettel
+ was assumed to be missing. A workaround is to restart the software.
+ If the .meta
file is deleted, metadata is now
+ calculated in the same way when the .meta
file is
+ non-existing at the start of the software.
* (bug) A link to the current zettel, only using a fragment (e.g.
[[Title|#title]]
) is now handled correctly as
a zettel link (and not as a link to external material).
* (minor) Allow zettel to be marked as “read only”.
- This is done through the metadata key read-only .
+ This is done through the metadata key read-only
.
* (bug) When renaming a zettel, check all boxes for the new zettel
identifier, not just the first one. Otherwise it will be possible to
shadow a read-only zettel from a next box, effectively modifying it.
* (minor) Add support for a configurable default value for metadata key
- visibility .
- * (bug) If list-page-size is set to a relatively small value and
- the authenticated user is not the owner, some zettel were not
- shown in the list of zettel or were not returned by the API.
+ visibility
.
+ * (bug) If list-page-size
is set to a relatively small value
+ and the authenticated user is not the owner, some zettel were
+ not shown in the list of zettel or were not returned by the API.
* (minor) Add support for new visibility “expert”.
An owner becomes an expert, if the runtime configuration key
- expert-mode is set to true.
+ expert-mode
is set to true.
* (major) Add support for computed zettel.
- These zettel have an identifier less than 0000000000100 .
- Most of them are only visible, if expert-mode is enabled.
+ These zettel have an identifier less than
+ 0000000000100
. Most of them are only visible, if
+ expert-mode
is enabled.
* (bug) Fixes a memory leak that results in too many open files after
approx. 125 reload operations.
* (major) Predefined templates for new zettel got an explicit value for
visibility: “login”. Please update these zettel if you
modified them.
- * (major) Rename key readonly of Zettelstore Startup
- Configuration to read-only-mode . This was done to
+ * (major) Rename key readonly
of Zettelstore Startup
+ Configuration to read-only-mode
. This was done to
avoid some confusion with the the zettel metadata key
- read-only . Please adapt your startup configuration.
+ read-only
. Please adapt your startup configuration.
Otherwise your Zettelstore will be accidentally writable.
* (minor) References starting with “./” and “../”
are treated as a local reference. Previously, only the prefix
“/” was treated as a local reference.
- * (major) Metadata key modified will be set automatically to the
- current local time if a zettel is updated through Zettelstore.
+ * (major) Metadata key modified
will be set automatically to
+ the current local time if a zettel is updated through Zettelstore.
If you used that key previously for your own, you should rename
it before you upgrade.
* (minor) The new visibility value “simple-expert” ensures that
many computed zettel are shown for new users. This is to enable
them to send useful bug reports.
@@ -711,16 +1260,16 @@
* (minor) Move zettel field "role" above "tags" and move "syntax" more to
"content".
* (minor) Rename zettel operation “clone” to “copy”.
* (major) All predefined HTML templates have now a visibility value
“expert”. If you want to see them as an non-expert
- owner, you must temporary enable expert-mode and change
- the visibility metadata value.
+ owner, you must temporary enable expert-mode
and
+ change the visibility
metadata value.
* (minor) Initial support for
[https://zettelkasten.de/posts/tags/folgezettel/|Folgezettel]. If
you click on “Folge” (detail view or info view), a new
- zettel is created with a reference (precursor ) to the
+ zettel is created with a reference (precursor
) to the
original zettel. Title, role, tags, and syntax are copied from the
original zettel.
* (major) Most predefined zettel have a title prefix of
“Zettelstore”.
* (minor) If started in simple mode, e.g. via double click or without any
@@ -728,32 +1277,33 @@
terminal, there is a hint about opening the web browser and use
a specific URL. A Welcome zettel is created, to give some
more information. (This change also applies to the server itself,
but it is more suited to the WebUI user.)
-
+
Changes for Version 0.0.7 (2020-11-24)
* With this version, Zettelstore and this manual got a new license, the
[https://joinup.ec.europa.eu/collection/eupl|European Union Public
Licence] (EUPL), version 1.2 or later. Nothing else changed. If you want
to stay with the old licenses (AGPLv3+, CC BY-SA 4.0), you are free to
fork from the previous version.
-
+
Changes for Version 0.0.6 (2020-11-23)
Server
* (major) Rename identifier of Zettelstore Runtime Configuration to
- 00000000000100 (previously 00000000000001 ). This
- is done to gain some free identifier with smaller number to be
- used internally. If you customized this zettel, please make
- sure to rename it to the new identifier.
+ 00000000000100
(previously
+ 00000000000001
). This is done to gain some free
+ identifier with smaller number to be used internally. If you
+ customized this zettel, please make sure to rename it to the new
+ identifier.
* (major) Rename the two essential metadata keys of a user zettel to
- credential and user-id . The previous values were
- cred and ident . If you enabled user
- authentication and added some user zettel, make sure to change
- them accordingly. Otherwise these users will not authenticated any
- more.
+ credential
and user-id
. The previous
+ values were cred
and ident
. If you
+ enabled user authentication and added some user zettel, make sure
+ to change them accordingly. Otherwise these users will not
+ authenticated any more.
* (minor) Rename the scheme of the box URL where predefined zettel are
stored to “const”. The previous value was
“globals”.
Zettelmarkup
@@ -766,11 +1316,11 @@
in valid JSON content.
* (bug) All query parameters of selecting zettel must be true, regardless
if a specific key occurs more than one or not.
* (minor) Encode all inherited meta values in all formats except
“raw”. A meta value is called inherited if
- there is a key starting with default- in the
+ there is a key starting with default-
in the
Zettelstore Runtime Configuration . Applies to WebUI also.
* (minor) Automatic calculated identifier for headings (only for
“html”, “djson”, “native”
format and for the Web user interface). You can use this to
provide a zettel reference that links to the heading, without
@@ -790,72 +1340,75 @@
references”). When a local reference is displayed as an URL
on the WebUI, it will not opened in a new window/tab. They will
receive a local marker, when encoded as “djson”
or “native”. Local references are listed on the
Info page of each zettel.
- * (minor) Change the default value for some visual sugar putd after an
- external URL to &\#8599;&\#xfe0e;
+ * (minor) Change the default value for some visual sugar put after an
+ external URL to &\#8599;&\#xfe0e;
(“↗︎”). This affects the former key
- icon-material of the Zettelstore Runtime
- Configuration , which is renamed to marker-external .
+ icon-material
of the Zettelstore Runtime
+ Configuration , which is renamed to
+ marker-external
.
* (major) Allow multiple zettel to act as templates for creating new zettel.
All zettel with a role value “new-template” act as
a template to create a new zettel. The WebUI menu item
“New” changed to a drop-down list with all those
zettel, ordered by their identifier. All metadata keys with the
- prefix new- will be translated to a new or updated
+ prefix new-
will be translated to a new or updated
keys/value without that prefix. You can use this mechanism to
specify a role for the new zettel, or a different title. The title
of the template zettel is used in the drop-down list. The initial
template zettel “New Zettel” has now a different
- zettel identifier (now: 00000000091001 , was:
- 00000000040001 ). Please update it, if you changed that
- zettel.
+ zettel identifier (now: 00000000091001
, was:
+ 00000000040001
). Please update it, if you changed
+ that zettel.
Note: this feature was superseded in [#0_0_10|version 0.0.10]
by the “New Menu” zettel.
* (minor) When a page should be opened in a new windows (e.g. for external
references), the web browser is instructed to decouple the new
page from the previous one for privacy and security reasons. In
detail, the web browser is instructed to omit referrer information
and to omit a JS object linking to the page that contained the
external link.
* (minor) If the value of the Zettelstore Runtime Configuration key
- list-page-size is greater than zero, the number of WebUI
- list elements will be restricted and it is possible to change to
- the next/previous page to list more elements.
+ list-page-size
is greater than zero, the number of
+ WebUI list elements will be restricted and it is possible to
+ change to the next/previous page to list more elements.
* (minor) Change CSS to enhance reading: make line-height
a little smaller (previous: 1.6, now 1.4) and move list items to
the left.
-
+
Changes for Version 0.0.5 (2020-10-22)
* Application Programming Interface (API) to allow external software to
retrieve zettel data from the Zettelstore.
* Specify boxes, where zettel are stored, via an URL.
* Add support for a custom footer.
-
+
Changes for Version 0.0.4 (2020-09-11)
* Optional user authentication/authorization.
- * New sub-commands file (use Zettelstore as a command line filter),
- password (for authentication), and config .
+ * New sub-commands file
(use Zettelstore as a command line
+ filter), password
(for authentication), and
+ config
.
-
+
Changes for Version 0.0.3 (2020-08-31)
* Starting Zettelstore has been changed by introducing sub-commands.
This change is also reflected on the server installation procedures.
* Limitations on renaming zettel has been relaxed.
-
+
Changes for Version 0.0.2 (2020-08-28)
- * Configuration zettel now has ID 00000000000001 (previously:
- 00000000000000 ).
- * The zettel with ID 00000000000000 is no longer shown in any
+ * Configuration zettel now has ID 00000000000001
(previously:
+ 00000000000000
).
+ * The zettel with ID 00000000000000
is no longer shown in any
zettel list. If you changed the configuration zettel, you should rename it
manually in its file directory.
* Creating a new zettel is now done by cloning an existing zettel.
- To mimic the previous behaviour, a zettel with ID 00000000040001
- is introduced. You can change it if you need a different template zettel.
+ To mimic the previous behaviour, a zettel with ID
+ 00000000040001
is introduced. You can change it if you need
+ a different template zettel.
-
+
Changes for Version 0.0.1 (2020-08-21)
* Initial public release.
Index: www/download.wiki
==================================================================
--- www/download.wiki
+++ www/download.wiki
@@ -7,20 +7,20 @@
* 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.
ZIP-ped Executables
-Build: v0.6.0
(2022-08-11).
+Build: v0.17.0
(2024-03-04).
- * [/uv/zettelstore-0.6.0-linux-amd64.zip|Linux] (amd64)
- * [/uv/zettelstore-0.6.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
- * [/uv/zettelstore-0.6.0-windows-amd64.zip|Windows] (amd64)
- * [/uv/zettelstore-0.6.0-darwin-amd64.zip|macOS] (amd64)
- * [/uv/zettelstore-0.6.0-darwin-arm64.zip|macOS] (arm64, aka Apple silicon)
+ * [/uv/zettelstore-0.17.0-linux-amd64.zip|Linux] (amd64)
+ * [/uv/zettelstore-0.17.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
+ * [/uv/zettelstore-0.17.0-darwin-arm64.zip|macOS] (arm64)
+ * [/uv/zettelstore-0.17.0-darwin-amd64.zip|macOS] (amd64)
+ * [/uv/zettelstore-0.17.0-windows-amd64.zip|Windows] (amd64)
Unzip the appropriate file, install and execute Zettelstore according to the manual.
Zettel for the manual
As a starter, you can download the zettel for the manual
-[/uv/manual-0.6.0.zip|here].
+[/uv/manual-0.17.0.zip|here].
Just unzip the contained files and put them into your zettel folder or
configure a file box to read the zettel directly from the ZIP file.
Index: www/index.wiki
==================================================================
--- www/index.wiki
+++ www/index.wiki
@@ -14,31 +14,33 @@
zettelstore software, running in read-only mode.
The software, including the manual, is licensed under the
[/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)].
-[https://zettelstore.de/client|Zettelstore Client] provides client software to
-access Zettelstore via its API more easily,
-[https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed
-software, which often connects to Zettelstore via its API. Some of the software
-packages may be experimental.
+ * [https://t73f.de/r/zsc|Zettelstore Client] provides client software to
+ access Zettelstore via its API more easily.
+ * [https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed
+ software, which often connects to Zettelstore via its API. Some of the
+ software packages may be experimental.
+ * [https://t73f.de/r/sx|Sx] provides an evaluator for symbolic
+ expressions, which is used for HTML templates and more.
-[https://twitter.com/zettelstore|Stay tuned] …
+[https://mastodon.social/tags/Zettelstore|Stay tuned] …
-Latest Release: 0.6.0 (2022-08-11)
+Latest Release: 0.17.0 (2024-03-04)
* [./download.wiki|Download]
- * [./changes.wiki#0_6|Change summary]
- * [/timeline?p=v0.6.0&bt=v0.5.0&y=ci|Check-ins for version 0.6.0],
- [/vdiff?to=v0.6.0&from=v0.5.0|content diff]
- * [/timeline?df=v0.6.0&y=ci|Check-ins derived from the 0.6.0 release],
- [/vdiff?from=v0.6.0&to=trunk|content diff]
+ * [./changes.wiki#0_17|Change summary]
+ * [/timeline?p=v0.17.0&bt=v0.16.0&y=ci|Check-ins for version 0.17.0],
+ [/vdiff?to=v0.17.0&from=v0.16.0|content diff]
+ * [/timeline?df=v0.17.0&y=ci|Check-ins derived from the 0.17.0 release],
+ [/vdiff?from=v0.17.0&to=trunk|content diff]
* [./plan.wiki|Limitations and planned improvements]
* [/timeline?t=release|Timeline of all past releases]
Build instructions
-Just install [https://golang.org/dl/|Go] and some Go-based tools. Please read
+Just install [https://go.dev/dl/|Go] and some Go-based tools. Please read
the [./build.md|instructions] for details.
* [/dir?ci=trunk|Source code]
* [/download|Download the source code] as a tarball or a ZIP file
(you must [/login|login] as user "anonymous").
Index: www/plan.wiki
==================================================================
--- www/plan.wiki
+++ www/plan.wiki
@@ -1,19 +1,12 @@
Limitations and planned improvements
Here is a list of some shortcomings of Zettelstore.
They are planned to be solved.
-Serious limitations
- * Content with binary data (e.g. a GIF, PNG, or JPG file) cannot be created
- nor modified via the standard web interface. As a workaround, you should
- put your file into the directory where your zettel are stored. Make sure
- that the file name starts with unique 14 digits that make up the zettel
- identifier.
- * …
-
-Smaller limitations
+ * Zettelstore must have indexed all zettel to make use of queries.
+ Otherwise not all zettel may be returned.
* Quoted attribute values are not yet supported in Zettelmarkup:
{key="value with space"}
.
* The horizontal tab character (U+0009 ) is not supported.
* Missing support for citation keys.
* Changing the content syntax is not reflected in file extension.
ADDED zettel/content.go
Index: zettel/content.go
==================================================================
--- zettel/content.go
+++ zettel/content.go
@@ -0,0 +1,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/zsc/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 !input.IsSpace(inp.Ch) {
+ 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 zettel/content_test.go
Index: zettel/content_test.go
==================================================================
--- zettel/content_test.go
+++ zettel/content_test.go
@@ -0,0 +1,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/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 zettel/id/digraph.go
Index: zettel/id/digraph.go
==================================================================
--- zettel/id/digraph.go
+++ zettel/id/digraph.go
@@ -0,0 +1,239 @@
+//-----------------------------------------------------------------------------
+// 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 id
+
+import (
+ "maps"
+ "slices"
+)
+
+// Digraph relates zettel identifier in a directional way.
+type Digraph map[Zid]Set
+
+// AddVertex adds an edge / vertex to the digraph.
+func (dg Digraph) AddVertex(zid Zid) Digraph {
+ if dg == nil {
+ return Digraph{zid: nil}
+ }
+ if _, found := dg[zid]; !found {
+ dg[zid] = nil
+ }
+ return dg
+}
+
+// RemoveVertex removes a vertex and all its edges from the digraph.
+func (dg Digraph) RemoveVertex(zid Zid) {
+ if len(dg) > 0 {
+ delete(dg, zid)
+ for vertex, closure := range dg {
+ dg[vertex] = closure.Remove(zid)
+ }
+ }
+}
+
+// AddEdge adds a connection from `zid1` to `zid2`.
+// Both vertices must be added before. Otherwise the function may panic.
+func (dg Digraph) AddEdge(fromZid, toZid Zid) Digraph {
+ if dg == nil {
+ return Digraph{fromZid: Set(nil).Add(toZid), toZid: nil}
+ }
+ dg[fromZid] = dg[fromZid].Add(toZid)
+ return dg
+}
+
+// AddEgdes adds all given `Edge`s to the digraph.
+//
+// In contrast to `AddEdge` the vertices must not exist before.
+func (dg Digraph) AddEgdes(edges EdgeSlice) Digraph {
+ if dg == nil {
+ if len(edges) == 0 {
+ return nil
+ }
+ dg = make(Digraph, len(edges))
+ }
+ for _, edge := range edges {
+ dg = dg.AddVertex(edge.From)
+ dg = dg.AddVertex(edge.To)
+ dg = dg.AddEdge(edge.From, edge.To)
+ }
+ return dg
+}
+
+// Equal returns true if both digraphs have the same vertices and edges.
+func (dg Digraph) Equal(other Digraph) bool {
+ return maps.EqualFunc(dg, other, func(cg, co Set) bool { return cg.Equal(co) })
+}
+
+// Clone a digraph.
+func (dg Digraph) Clone() Digraph {
+ if len(dg) == 0 {
+ return nil
+ }
+ copyDG := make(Digraph, len(dg))
+ for vertex, closure := range dg {
+ copyDG[vertex] = closure.Clone()
+ }
+ return copyDG
+}
+
+// HasVertex returns true, if `zid` is a vertex of the digraph.
+func (dg Digraph) HasVertex(zid Zid) bool {
+ if len(dg) == 0 {
+ return false
+ }
+ _, found := dg[zid]
+ return found
+}
+
+// Vertices returns the set of all vertices.
+func (dg Digraph) Vertices() Set {
+ if len(dg) == 0 {
+ return nil
+ }
+ verts := NewSetCap(len(dg))
+ for vert := range dg {
+ verts.Add(vert)
+ }
+ return verts
+}
+
+// Edges returns an unsorted slice of the edges of the digraph.
+func (dg Digraph) Edges() (es EdgeSlice) {
+ for vert, closure := range dg {
+ for next := range closure {
+ es = append(es, Edge{From: vert, To: next})
+ }
+ }
+ return es
+}
+
+// Originators will return the set of all vertices that are not referenced
+// a the to-part of an edge.
+func (dg Digraph) Originators() Set {
+ if len(dg) == 0 {
+ return nil
+ }
+ origs := dg.Vertices()
+ for _, closure := range dg {
+ origs.Substract(closure)
+ }
+ return origs
+}
+
+// Terminators returns the set of all vertices that does not reference
+// other vertices.
+func (dg Digraph) Terminators() (terms Set) {
+ for vert, closure := range dg {
+ if len(closure) == 0 {
+ terms = terms.Add(vert)
+ }
+ }
+ return terms
+}
+
+// TransitiveClosure calculates the sub-graph that is reachable from `zid`.
+func (dg Digraph) TransitiveClosure(zid Zid) (tc Digraph) {
+ if len(dg) == 0 {
+ return nil
+ }
+ var marked Set
+ stack := Slice{zid}
+ for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 {
+ curr := stack[pos]
+ stack = stack[:pos]
+ if marked.Contains(curr) {
+ continue
+ }
+ tc = tc.AddVertex(curr)
+ for next := range dg[curr] {
+ tc = tc.AddVertex(next)
+ tc = tc.AddEdge(curr, next)
+ stack = append(stack, next)
+ }
+ marked = marked.Add(curr)
+ }
+ return tc
+}
+
+// ReachableVertices calculates the set of all vertices that are reachable
+// from the given `zid`.
+func (dg Digraph) ReachableVertices(zid Zid) (tc Set) {
+ if len(dg) == 0 {
+ return nil
+ }
+ stack := dg[zid].Sorted()
+ for last := len(stack) - 1; last >= 0; last = len(stack) - 1 {
+ curr := stack[last]
+ stack = stack[:last]
+ if tc.Contains(curr) {
+ continue
+ }
+ closure, found := dg[curr]
+ if !found {
+ continue
+ }
+ tc = tc.Add(curr)
+ for next := range closure {
+ stack = append(stack, next)
+ }
+ }
+ return tc
+}
+
+// IsDAG returns a vertex and false, if the graph has a cycle containing the vertex.
+func (dg Digraph) IsDAG() (Zid, bool) {
+ for vertex := range dg {
+ if dg.ReachableVertices(vertex).Contains(vertex) {
+ return vertex, false
+ }
+ }
+ return Invalid, true
+}
+
+// Reverse returns a graph with reversed edges.
+func (dg Digraph) Reverse() (revDg Digraph) {
+ for vertex, closure := range dg {
+ revDg = revDg.AddVertex(vertex)
+ for next := range closure {
+ revDg = revDg.AddVertex(next)
+ revDg = revDg.AddEdge(next, vertex)
+ }
+ }
+ return revDg
+}
+
+// SortReverse returns a deterministic, topological, reverse sort of the
+// digraph.
+//
+// Works only if digraph is a DAG. Otherwise the algorithm will not terminate
+// or returns an arbitrary value.
+func (dg Digraph) SortReverse() (sl Slice) {
+ if len(dg) == 0 {
+ return nil
+ }
+ tempDg := dg.Clone()
+ for len(tempDg) > 0 {
+ terms := tempDg.Terminators()
+ if len(terms) == 0 {
+ break
+ }
+ termSlice := terms.Sorted()
+ slices.Reverse(termSlice)
+ sl = append(sl, termSlice...)
+ for t := range terms {
+ tempDg.RemoveVertex(t)
+ }
+ }
+ return sl
+}
ADDED zettel/id/digraph_test.go
Index: zettel/id/digraph_test.go
==================================================================
--- zettel/id/digraph_test.go
+++ zettel/id/digraph_test.go
@@ -0,0 +1,178 @@
+//-----------------------------------------------------------------------------
+// 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 id_test
+
+import (
+ "testing"
+
+ "zettelstore.de/z/zettel/id"
+)
+
+type zps = id.EdgeSlice
+
+func createDigraph(pairs zps) (dg id.Digraph) {
+ return dg.AddEgdes(pairs)
+}
+
+func TestDigraphOriginators(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ name string
+ dg id.EdgeSlice
+ orig id.Set
+ term id.Set
+ }{
+ {"empty", nil, nil, nil},
+ {"single", zps{{0, 1}}, id.NewSet(0), id.NewSet(1)},
+ {"chain", zps{{0, 1}, {1, 2}, {2, 3}}, id.NewSet(0), id.NewSet(3)},
+ }
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ dg := createDigraph(tc.dg)
+ if got := dg.Originators(); !tc.orig.Equal(got) {
+ t.Errorf("Originators: expected:\n%v, but got:\n%v", tc.orig, got)
+ }
+ if got := dg.Terminators(); !tc.term.Equal(got) {
+ t.Errorf("Termintors: expected:\n%v, but got:\n%v", tc.orig, got)
+ }
+ })
+ }
+}
+
+func TestDigraphReachableVertices(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ name string
+ pairs id.EdgeSlice
+ start id.Zid
+ exp id.Set
+ }{
+ {"nil", nil, 0, nil},
+ {"0-2", zps{{1, 2}, {2, 3}}, 1, id.NewSet(2, 3)},
+ {"1,2", zps{{1, 2}, {2, 3}}, 2, id.NewSet(3)},
+ {"0-2,1-2", zps{{1, 2}, {2, 3}, {1, 3}}, 1, id.NewSet(2, 3)},
+ {"0-2,1-2/1", zps{{1, 2}, {2, 3}, {1, 3}}, 2, id.NewSet(3)},
+ {"0-2,1-2/2", zps{{1, 2}, {2, 3}, {1, 3}}, 3, nil},
+ {"0-2,1-2,3*", zps{{1, 2}, {2, 3}, {1, 3}, {4, 4}}, 1, id.NewSet(2, 3)},
+ }
+
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ dg := createDigraph(tc.pairs)
+ if got := dg.ReachableVertices(tc.start); !got.Equal(tc.exp) {
+ t.Errorf("\n%v, but got:\n%v", tc.exp, got)
+ }
+
+ })
+ }
+}
+
+func TestDigraphTransitiveClosure(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ name string
+ pairs id.EdgeSlice
+ start id.Zid
+ exp id.EdgeSlice
+ }{
+ {"nil", nil, 0, nil},
+ {"1-3", zps{{1, 2}, {2, 3}}, 1, zps{{1, 2}, {2, 3}}},
+ {"1,2", zps{{1, 1}, {2, 3}}, 2, zps{{2, 3}}},
+ {"0-2,1-2", zps{{1, 2}, {2, 3}, {1, 3}}, 1, zps{{1, 2}, {1, 3}, {2, 3}}},
+ {"0-2,1-2/1", zps{{1, 2}, {2, 3}, {1, 3}}, 1, zps{{1, 2}, {1, 3}, {2, 3}}},
+ {"0-2,1-2/2", zps{{1, 2}, {2, 3}, {1, 3}}, 2, zps{{2, 3}}},
+ {"0-2,1-2,3*", zps{{1, 2}, {2, 3}, {1, 3}, {4, 4}}, 1, zps{{1, 2}, {1, 3}, {2, 3}}},
+ }
+
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ dg := createDigraph(tc.pairs)
+ if got := dg.TransitiveClosure(tc.start).Edges().Sort(); !got.Equal(tc.exp) {
+ t.Errorf("\n%v, but got:\n%v", tc.exp, got)
+ }
+ })
+ }
+}
+
+func TestIsDAG(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ name string
+ dg id.EdgeSlice
+ exp bool
+ }{
+ {"empty", nil, true},
+ {"single-edge", zps{{1, 2}}, true},
+ {"single-loop", zps{{1, 1}}, false},
+ {"long-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 2}}, false},
+ }
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ if zid, got := createDigraph(tc.dg).IsDAG(); got != tc.exp {
+ t.Errorf("expected %v, but got %v (%v)", tc.exp, got, zid)
+ }
+ })
+ }
+}
+
+func TestDigraphReverse(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ name string
+ dg id.EdgeSlice
+ exp id.EdgeSlice
+ }{
+ {"empty", nil, nil},
+ {"single-edge", zps{{1, 2}}, zps{{2, 1}}},
+ {"single-loop", zps{{1, 1}}, zps{{1, 1}}},
+ {"end-loop", zps{{1, 2}, {2, 2}}, zps{{2, 1}, {2, 2}}},
+ {"long-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 2}}, zps{{2, 1}, {2, 5}, {3, 2}, {4, 3}, {5, 4}}},
+ {"sect-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 2}}, zps{{2, 1}, {2, 4}, {3, 2}, {4, 3}, {5, 4}}},
+ {"two-islands", zps{{1, 2}, {2, 3}, {4, 5}}, zps{{2, 1}, {3, 2}, {5, 4}}},
+ {"direct-indirect", zps{{1, 2}, {1, 3}, {3, 2}}, zps{{2, 1}, {2, 3}, {3, 1}}},
+ }
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ dg := createDigraph(tc.dg)
+ if got := dg.Reverse().Edges().Sort(); !got.Equal(tc.exp) {
+ t.Errorf("\n%v, but got:\n%v", tc.exp, got)
+ }
+ })
+ }
+}
+
+func TestDigraphSortReverse(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ name string
+ dg id.EdgeSlice
+ exp id.Slice
+ }{
+ {"empty", nil, nil},
+ {"single-edge", zps{{1, 2}}, id.Slice{2, 1}},
+ {"single-loop", zps{{1, 1}}, nil},
+ {"end-loop", zps{{1, 2}, {2, 2}}, id.Slice{}},
+ {"long-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 2}}, id.Slice{}},
+ {"sect-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 2}}, id.Slice{5}},
+ {"two-islands", zps{{1, 2}, {2, 3}, {4, 5}}, id.Slice{5, 3, 4, 2, 1}},
+ {"direct-indirect", zps{{1, 2}, {1, 3}, {3, 2}}, id.Slice{2, 3, 1}},
+ }
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ if got := createDigraph(tc.dg).SortReverse(); !got.Equal(tc.exp) {
+ t.Errorf("expected:\n%v, but got:\n%v", tc.exp, got)
+ }
+ })
+ }
+}
ADDED zettel/id/edge.go
Index: zettel/id/edge.go
==================================================================
--- zettel/id/edge.go
+++ zettel/id/edge.go
@@ -0,0 +1,49 @@
+//-----------------------------------------------------------------------------
+// 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 id
+
+import "slices"
+
+// Edge is a pair of to vertices.
+type Edge struct {
+ From, To Zid
+}
+
+// EdgeSlice is a slice of Edges
+type EdgeSlice []Edge
+
+// Equal return true if both slices are the same.
+func (es EdgeSlice) Equal(other EdgeSlice) bool {
+ return slices.Equal(es, other)
+}
+
+// Sort the slice.
+func (es EdgeSlice) Sort() EdgeSlice {
+ slices.SortFunc(es, func(e1, e2 Edge) int {
+ if e1.From < e2.From {
+ return -1
+ }
+ if e1.From > e2.From {
+ return 1
+ }
+ if e1.To < e2.To {
+ return -1
+ }
+ if e1.To > e2.To {
+ return 1
+ }
+ return 0
+ })
+ return es
+}
ADDED zettel/id/id.go
Index: zettel/id/id.go
==================================================================
--- zettel/id/id.go
+++ zettel/id/id.go
@@ -0,0 +1,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 id provides zettel specific types, constants, and functions about
+// zettel identifier.
+package id
+
+import (
+ "strconv"
+ "time"
+
+ "t73f.de/r/zsc/api"
+)
+
+// Zid is the internal identifier of a zettel. Typically, it is a
+// time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer.
+// A zettelstore implementation should try to set the last two digits to zero,
+// e.g. the seconds should be zero,
+type Zid uint64
+
+// Some important ZettelIDs.
+const (
+ Invalid = Zid(0) // Invalid is a Zid that will never be valid
+)
+
+// ZettelIDs that are used as Zid more than once.
+//
+// Note: if you change some values, ensure that you also change them in the
+// Constant box. They are mentioned there literally, because these
+// constants are not available there.
+var (
+ ConfigurationZid = MustParse(api.ZidConfiguration)
+ BaseTemplateZid = MustParse(api.ZidBaseTemplate)
+ LoginTemplateZid = MustParse(api.ZidLoginTemplate)
+ ListTemplateZid = MustParse(api.ZidListTemplate)
+ ZettelTemplateZid = MustParse(api.ZidZettelTemplate)
+ InfoTemplateZid = MustParse(api.ZidInfoTemplate)
+ FormTemplateZid = MustParse(api.ZidFormTemplate)
+ RenameTemplateZid = MustParse(api.ZidRenameTemplate)
+ DeleteTemplateZid = MustParse(api.ZidDeleteTemplate)
+ ErrorTemplateZid = MustParse(api.ZidErrorTemplate)
+ StartSxnZid = MustParse(api.ZidSxnStart)
+ BaseSxnZid = MustParse(api.ZidSxnBase)
+ PreludeSxnZid = MustParse(api.ZidSxnPrelude)
+ EmojiZid = MustParse(api.ZidEmoji)
+ TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate)
+ DefaultHomeZid = MustParse(api.ZidDefaultHome)
+)
+
+const maxZid = 99999999999999
+
+// ParseUint interprets a string as a possible zettel identifier
+// and returns its integer value.
+func ParseUint(s string) (uint64, error) {
+ res, err := strconv.ParseUint(s, 10, 47)
+ if err != nil {
+ return 0, err
+ }
+ if res == 0 || res > maxZid {
+ return res, strconv.ErrRange
+ }
+ return res, nil
+}
+
+// Parse interprets a string as a zettel identification and
+// returns its value.
+func Parse(s string) (Zid, error) {
+ if len(s) != 14 {
+ return Invalid, strconv.ErrSyntax
+ }
+ res, err := ParseUint(s)
+ if err != nil {
+ return Invalid, err
+ }
+ return Zid(res), nil
+}
+
+// MustParse tries to interpret a string as a zettel identifier and returns
+// its value or panics otherwise.
+func MustParse(s api.ZettelID) Zid {
+ zid, err := Parse(string(s))
+ if err == nil {
+ return zid
+ }
+ panic(err)
+}
+
+// String converts the zettel identification to a string of 14 digits.
+// Only defined for valid ids.
+func (zid Zid) String() string {
+ var result [14]byte
+ zid.toByteArray(&result)
+ return string(result[:])
+}
+
+// ZettelID return the zettel identification as a api.ZettelID.
+func (zid Zid) ZettelID() api.ZettelID { return api.ZettelID(zid.String()) }
+
+// Bytes converts the zettel identification to a byte slice of 14 digits.
+// Only defined for valid ids.
+func (zid Zid) Bytes() []byte {
+ var result [14]byte
+ zid.toByteArray(&result)
+ return result[:]
+}
+
+// toByteArray converts the Zid into a fixed byte array, usable for printing.
+//
+// Based on idea by Daniel Lemire: "Converting integers to fix-digit representations quickly"
+// https://lemire.me/blog/2021/11/18/converting-integers-to-fix-digit-representations-quickly/
+func (zid Zid) toByteArray(result *[14]byte) {
+ date := uint64(zid) / 1000000
+ fullyear := date / 10000
+ century, year := fullyear/100, fullyear%100
+ monthday := date % 10000
+ month, day := monthday/100, monthday%100
+ time := uint64(zid) % 1000000
+ hmtime, second := time/100, time%100
+ hour, minute := hmtime/100, hmtime%100
+
+ result[0] = byte(century/10) + '0'
+ result[1] = byte(century%10) + '0'
+ result[2] = byte(year/10) + '0'
+ result[3] = byte(year%10) + '0'
+ result[4] = byte(month/10) + '0'
+ result[5] = byte(month%10) + '0'
+ result[6] = byte(day/10) + '0'
+ result[7] = byte(day%10) + '0'
+ result[8] = byte(hour/10) + '0'
+ result[9] = byte(hour%10) + '0'
+ result[10] = byte(minute/10) + '0'
+ result[11] = byte(minute%10) + '0'
+ result[12] = byte(second/10) + '0'
+ result[13] = byte(second%10) + '0'
+}
+
+// IsValid determines if zettel id is a valid one, e.g. consists of max. 14 digits.
+func (zid Zid) IsValid() bool { return 0 < zid && zid <= maxZid }
+
+// TimestampLayout to transform a date into a Zid and into other internal dates.
+const TimestampLayout = "20060102150405"
+
+// New returns a new zettel id based on the current time.
+func New(withSeconds bool) Zid {
+ now := time.Now().Local()
+ var s string
+ if withSeconds {
+ s = now.Format(TimestampLayout)
+ } else {
+ s = now.Format("20060102150400")
+ }
+ res, err := Parse(s)
+ if err != nil {
+ panic(err)
+ }
+ return res
+}
ADDED zettel/id/id_test.go
Index: zettel/id/id_test.go
==================================================================
--- zettel/id/id_test.go
+++ zettel/id/id_test.go
@@ -0,0 +1,92 @@
+//-----------------------------------------------------------------------------
+// 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 id_test provides unit tests for testing zettel id specific functions.
+package id_test
+
+import (
+ "testing"
+
+ "zettelstore.de/z/zettel/id"
+)
+
+func TestIsValid(t *testing.T) {
+ t.Parallel()
+ validIDs := []string{
+ "00000000000001",
+ "00000000000020",
+ "00000000000300",
+ "00000000004000",
+ "00000000050000",
+ "00000000600000",
+ "00000007000000",
+ "00000080000000",
+ "00000900000000",
+ "00001000000000",
+ "00020000000000",
+ "00300000000000",
+ "04000000000000",
+ "50000000000000",
+ "99999999999999",
+ "00001007030200",
+ "20200310195100",
+ "12345678901234",
+ }
+
+ for i, sid := range validIDs {
+ zid, err := id.Parse(sid)
+ if err != nil {
+ t.Errorf("i=%d: sid=%q is not valid, but should be. err=%v", i, sid, err)
+ }
+ s := zid.String()
+ if s != sid {
+ t.Errorf(
+ "i=%d: zid=%v does not format to %q, but to %q", i, zid, sid, s)
+ }
+ }
+
+ invalidIDs := []string{
+ "", "0", "a",
+ "00000000000000",
+ "0000000000000a",
+ "000000000000000",
+ "20200310T195100",
+ "+1234567890123",
+ }
+
+ for i, sid := range invalidIDs {
+ if zid, err := id.Parse(sid); err == nil {
+ t.Errorf("i=%d: sid=%q is valid (zid=%s), but should not be", i, sid, zid)
+ }
+ }
+}
+
+var sResult string // to disable compiler optimization in loop below
+
+func BenchmarkString(b *testing.B) {
+ var s string
+ for range b.N {
+ s = id.Zid(12345678901200).String()
+ }
+ sResult = s
+}
+
+var bResult []byte // to disable compiler optimization in loop below
+
+func BenchmarkBytes(b *testing.B) {
+ var bs []byte
+ for range b.N {
+ bs = id.Zid(12345678901200).Bytes()
+ }
+ bResult = bs
+}
ADDED zettel/id/set.go
Index: zettel/id/set.go
==================================================================
--- zettel/id/set.go
+++ zettel/id/set.go
@@ -0,0 +1,181 @@
+//-----------------------------------------------------------------------------
+// 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 id
+
+import (
+ "maps"
+ "strings"
+)
+
+// Set is a set of zettel identifier
+type Set map[Zid]struct{}
+
+// String returns a string representation of the map.
+func (s Set) String() string {
+ if s == nil {
+ return "{}"
+ }
+ var sb strings.Builder
+ sb.WriteByte('{')
+ for i, zid := range s.Sorted() {
+ if i > 0 {
+ sb.WriteByte(' ')
+ }
+ sb.Write(zid.Bytes())
+ }
+ sb.WriteByte('}')
+ return sb.String()
+}
+
+// NewSet returns a new set of identifier with the given initial values.
+func NewSet(zids ...Zid) Set {
+ l := len(zids)
+ if l < 8 {
+ l = 8
+ }
+ result := make(Set, l)
+ result.CopySlice(zids)
+ return result
+}
+
+// NewSetCap returns a new set of identifier with the given capacity and initial values.
+func NewSetCap(c int, zids ...Zid) Set {
+ l := len(zids)
+ if c < l {
+ c = l
+ }
+ if c < 8 {
+ c = 8
+ }
+ result := make(Set, c)
+ result.CopySlice(zids)
+ return result
+}
+
+// Clone returns a copy of the given set.
+func (s Set) Clone() Set {
+ if len(s) == 0 {
+ return nil
+ }
+ return maps.Clone(s)
+}
+
+// Add adds a Add to the set.
+func (s Set) Add(zid Zid) Set {
+ if s == nil {
+ return NewSet(zid)
+ }
+ s[zid] = struct{}{}
+ return s
+}
+
+// Contains return true if the set is non-nil and the set contains the given Zettel identifier.
+func (s Set) Contains(zid Zid) bool {
+ if s != nil {
+ _, found := s[zid]
+ return found
+ }
+ return false
+}
+
+// ContainsOrNil return true if the set is nil or if the set contains the given Zettel identifier.
+func (s Set) ContainsOrNil(zid Zid) bool {
+ if s != nil {
+ _, found := s[zid]
+ return found
+ }
+ return true
+}
+
+// Copy adds all member from the other set.
+func (s Set) Copy(other Set) Set {
+ if s == nil {
+ if len(other) == 0 {
+ return nil
+ }
+ s = NewSetCap(len(other))
+ }
+ maps.Copy(s, other)
+ return s
+}
+
+// CopySlice adds all identifier of the given slice to the set.
+func (s Set) CopySlice(sl Slice) Set {
+ if s == nil {
+ s = NewSetCap(len(sl))
+ }
+ for _, zid := range sl {
+ s[zid] = struct{}{}
+ }
+ return s
+}
+
+// Sorted returns the set as a sorted slice of zettel identifier.
+func (s Set) Sorted() Slice {
+ if l := len(s); l > 0 {
+ result := make(Slice, 0, l)
+ for zid := range s {
+ result = append(result, zid)
+ }
+ result.Sort()
+ return result
+ }
+ return nil
+}
+
+// IntersectOrSet removes all zettel identifier that are not in the other set.
+// Both sets can be modified by this method. One of them is the set returned.
+// It contains the intersection of both, if s is not nil.
+//
+// If s == nil, then the other set is always returned.
+func (s Set) IntersectOrSet(other Set) Set {
+ if s == nil {
+ return other
+ }
+ if len(s) > len(other) {
+ s, other = other, s
+ }
+ for zid := range s {
+ _, otherOk := other[zid]
+ if !otherOk {
+ delete(s, zid)
+ }
+ }
+ return s
+}
+
+// Substract removes all zettel identifier from 's' that are in the set 'other'.
+func (s Set) Substract(other Set) {
+ if s == nil || other == nil {
+ return
+ }
+ for zid := range other {
+ delete(s, zid)
+ }
+}
+
+// Remove the identifier from the set.
+func (s Set) Remove(zid Zid) Set {
+ if len(s) == 0 {
+ return nil
+ }
+ delete(s, zid)
+ if len(s) == 0 {
+ return nil
+ }
+ return s
+}
+
+// Equal returns true if the other set is equal to the given set.
+func (s Set) Equal(other Set) bool { return maps.Equal(s, other) }
ADDED zettel/id/set_test.go
Index: zettel/id/set_test.go
==================================================================
--- zettel/id/set_test.go
+++ zettel/id/set_test.go
@@ -0,0 +1,152 @@
+//-----------------------------------------------------------------------------
+// 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 id_test
+
+import (
+ "testing"
+
+ "zettelstore.de/z/zettel/id"
+)
+
+func TestSetContains(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ s id.Set
+ zid id.Zid
+ exp bool
+ }{
+ {nil, id.Invalid, true},
+ {nil, 14, true},
+ {id.NewSet(), id.Invalid, false},
+ {id.NewSet(), 1, false},
+ {id.NewSet(), id.Invalid, false},
+ {id.NewSet(1), 1, true},
+ }
+ for i, tc := range testcases {
+ got := tc.s.ContainsOrNil(tc.zid)
+ if got != tc.exp {
+ t.Errorf("%d: %v.Contains(%v) == %v, but got %v", i, tc.s, tc.zid, tc.exp, got)
+ }
+ }
+}
+
+func TestSetAdd(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ s1, s2 id.Set
+ exp id.Slice
+ }{
+ {nil, nil, nil},
+ {id.NewSet(), nil, nil},
+ {id.NewSet(), id.NewSet(), nil},
+ {nil, id.NewSet(1), id.Slice{1}},
+ {id.NewSet(1), nil, id.Slice{1}},
+ {id.NewSet(1), id.NewSet(), id.Slice{1}},
+ {id.NewSet(1), id.NewSet(2), id.Slice{1, 2}},
+ {id.NewSet(1), id.NewSet(1), id.Slice{1}},
+ }
+ for i, tc := range testcases {
+ sl1 := tc.s1.Sorted()
+ sl2 := tc.s2.Sorted()
+ got := tc.s1.Copy(tc.s2).Sorted()
+ if !got.Equal(tc.exp) {
+ t.Errorf("%d: %v.Add(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
+ }
+ }
+}
+
+func TestSetSorted(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ set id.Set
+ exp id.Slice
+ }{
+ {nil, nil},
+ {id.NewSet(), nil},
+ {id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}},
+ }
+ for i, tc := range testcases {
+ got := tc.set.Sorted()
+ if !got.Equal(tc.exp) {
+ t.Errorf("%d: %v.Sorted() should be %v, but got %v", i, tc.set, tc.exp, got)
+ }
+ }
+}
+
+func TestSetIntersectOrSet(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ s1, s2 id.Set
+ exp id.Slice
+ }{
+ {nil, nil, nil},
+ {id.NewSet(), nil, nil},
+ {nil, id.NewSet(), nil},
+ {id.NewSet(), id.NewSet(), nil},
+ {id.NewSet(1), nil, nil},
+ {nil, id.NewSet(1), id.Slice{1}},
+ {id.NewSet(1), id.NewSet(), nil},
+ {id.NewSet(), id.NewSet(1), nil},
+ {id.NewSet(1), id.NewSet(2), nil},
+ {id.NewSet(2), id.NewSet(1), nil},
+ {id.NewSet(1), id.NewSet(1), id.Slice{1}},
+ }
+ for i, tc := range testcases {
+ sl1 := tc.s1.Sorted()
+ sl2 := tc.s2.Sorted()
+ got := tc.s1.IntersectOrSet(tc.s2).Sorted()
+ if !got.Equal(tc.exp) {
+ t.Errorf("%d: %v.IntersectOrSet(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
+ }
+ }
+}
+
+func TestSetRemove(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ s1, s2 id.Set
+ exp id.Slice
+ }{
+ {nil, nil, nil},
+ {id.NewSet(), nil, nil},
+ {id.NewSet(), id.NewSet(), nil},
+ {id.NewSet(1), nil, id.Slice{1}},
+ {id.NewSet(1), id.NewSet(), id.Slice{1}},
+ {id.NewSet(1), id.NewSet(2), id.Slice{1}},
+ {id.NewSet(1), id.NewSet(1), id.Slice{}},
+ }
+ for i, tc := range testcases {
+ sl1 := tc.s1.Sorted()
+ sl2 := tc.s2.Sorted()
+ newS1 := id.NewSet(sl1...)
+ newS1.Substract(tc.s2)
+ got := newS1.Sorted()
+ if !got.Equal(tc.exp) {
+ t.Errorf("%d: %v.Remove(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
+ }
+ }
+}
+
+// func BenchmarkSet(b *testing.B) {
+// s := id.Set{}
+// for range b.N {
+// s[id.Zid(i)] = true
+// }
+// }
+func BenchmarkSet(b *testing.B) {
+ s := id.Set{}
+ for i := range b.N {
+ s[id.Zid(i)] = struct{}{}
+ }
+}
ADDED zettel/id/slice.go
Index: zettel/id/slice.go
==================================================================
--- zettel/id/slice.go
+++ zettel/id/slice.go
@@ -0,0 +1,46 @@
+//-----------------------------------------------------------------------------
+// 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 id
+
+import (
+ "slices"
+ "strings"
+)
+
+// Slice is a sequence of zettel identifier. A special case is a sorted slice.
+type Slice []Zid
+
+// Sort a slice of Zids.
+func (zs Slice) Sort() { slices.Sort(zs) }
+
+// Clone a zettel identifier slice
+func (zs Slice) Clone() Slice { return slices.Clone(zs) }
+
+// Equal reports whether zs and other are the same length and contain the samle zettel
+// identifier. A nil argument is equivalent to an empty slice.
+func (zs Slice) Equal(other Slice) bool { return slices.Equal(zs, other) }
+
+func (zs Slice) String() string {
+ if len(zs) == 0 {
+ return ""
+ }
+ var sb strings.Builder
+ for i, zid := range zs {
+ if i > 0 {
+ sb.WriteByte(' ')
+ }
+ sb.WriteString(zid.String())
+ }
+ return sb.String()
+}
ADDED zettel/id/slice_test.go
Index: zettel/id/slice_test.go
==================================================================
--- zettel/id/slice_test.go
+++ zettel/id/slice_test.go
@@ -0,0 +1,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 id_test
+
+import (
+ "testing"
+
+ "zettelstore.de/z/zettel/id"
+)
+
+func TestSliceSort(t *testing.T) {
+ t.Parallel()
+ zs := id.Slice{9, 4, 6, 1, 7}
+ zs.Sort()
+ exp := id.Slice{1, 4, 6, 7, 9}
+ if !zs.Equal(exp) {
+ t.Errorf("Slice.Sort did not work. Expected %v, got %v", exp, zs)
+ }
+}
+
+func TestCopy(t *testing.T) {
+ t.Parallel()
+ var orig id.Slice
+ got := orig.Clone()
+ if got != nil {
+ t.Errorf("Nil copy resulted in %v", got)
+ }
+ orig = id.Slice{9, 4, 6, 1, 7}
+ got = orig.Clone()
+ if !orig.Equal(got) {
+ t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got)
+ }
+}
+
+func TestSliceEqual(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ s1, s2 id.Slice
+ exp bool
+ }{
+ {nil, nil, true},
+ {nil, id.Slice{}, true},
+ {nil, id.Slice{1}, false},
+ {id.Slice{1}, id.Slice{1}, true},
+ {id.Slice{1}, id.Slice{2}, false},
+ {id.Slice{1, 2}, id.Slice{2, 1}, false},
+ {id.Slice{1, 2}, id.Slice{1, 2}, true},
+ }
+ for i, tc := range testcases {
+ got := tc.s1.Equal(tc.s2)
+ if got != tc.exp {
+ t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s1, tc.s2, tc.exp, got)
+ }
+ got = tc.s2.Equal(tc.s1)
+ if got != tc.exp {
+ t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got)
+ }
+ }
+}
+
+func TestSliceString(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ in id.Slice
+ exp string
+ }{
+ {nil, ""},
+ {id.Slice{}, ""},
+ {id.Slice{1}, "00000000000001"},
+ {id.Slice{1, 2}, "00000000000001 00000000000002"},
+ }
+ for i, tc := range testcases {
+ got := tc.in.String()
+ if got != tc.exp {
+ t.Errorf("%d/%v: expected %q, but got %q", i, tc.in, tc.exp, got)
+ }
+ }
+}
ADDED zettel/meta/collection.go
Index: zettel/meta/collection.go
==================================================================
--- zettel/meta/collection.go
+++ zettel/meta/collection.go
@@ -0,0 +1,113 @@
+//-----------------------------------------------------------------------------
+// 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 meta
+
+import "sort"
+
+// Arrangement stores metadata within its categories.
+// Typecally a category might be a tag name, a role name, a syntax value.
+type Arrangement map[string][]*Meta
+
+// CreateArrangement by inspecting a given key and use the found
+// value as a category.
+func CreateArrangement(metaList []*Meta, key string) Arrangement {
+ if len(metaList) == 0 {
+ return nil
+ }
+ descr := Type(key)
+ if descr == nil {
+ return nil
+ }
+ if descr.IsSet {
+ return createSetArrangement(metaList, key)
+ }
+ return createSimplearrangement(metaList, key)
+}
+
+func createSetArrangement(metaList []*Meta, key string) Arrangement {
+ a := make(Arrangement)
+ for _, m := range metaList {
+ if vals, ok := m.GetList(key); ok {
+ for _, val := range vals {
+ a[val] = append(a[val], m)
+ }
+ }
+ }
+ return a
+}
+
+func createSimplearrangement(metaList []*Meta, key string) Arrangement {
+ a := make(Arrangement)
+ for _, m := range metaList {
+ if val, ok := m.Get(key); ok && val != "" {
+ a[val] = append(a[val], m)
+ }
+ }
+ return a
+}
+
+// Counted returns the list of categories, together with the number of
+// metadata for each category.
+func (a Arrangement) Counted() CountedCategories {
+ if len(a) == 0 {
+ return nil
+ }
+ result := make(CountedCategories, 0, len(a))
+ for cat, metas := range a {
+ result = append(result, CountedCategory{Name: cat, Count: len(metas)})
+ }
+ return result
+}
+
+// CountedCategory contains of a name and the number how much this name occured
+// somewhere.
+type CountedCategory struct {
+ Name string
+ Count int
+}
+
+// CountedCategories is the list of CountedCategories.
+// Every name must occur only once.
+type CountedCategories []CountedCategory
+
+// SortByName sorts the list by the name attribute.
+// Since each name must occur only once, two CountedCategories cannot have
+// the same name.
+func (ccs CountedCategories) SortByName() {
+ sort.Slice(ccs, func(i, j int) bool { return ccs[i].Name < ccs[j].Name })
+}
+
+// SortByCount sorts the list by the count attribute, descending.
+// If two counts are equal, elements are sorted by name.
+func (ccs CountedCategories) SortByCount() {
+ sort.Slice(ccs, func(i, j int) bool {
+ iCount, jCount := ccs[i].Count, ccs[j].Count
+ if iCount > jCount {
+ return true
+ }
+ if iCount == jCount {
+ return ccs[i].Name < ccs[j].Name
+ }
+ return false
+ })
+}
+
+// Categories returns just the category names.
+func (ccs CountedCategories) Categories() []string {
+ result := make([]string, len(ccs))
+ for i, cc := range ccs {
+ result[i] = cc.Name
+ }
+ return result
+}
ADDED zettel/meta/meta.go
Index: zettel/meta/meta.go
==================================================================
--- zettel/meta/meta.go
+++ zettel/meta/meta.go
@@ -0,0 +1,418 @@
+//-----------------------------------------------------------------------------
+// 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 meta provides the zettel specific type 'meta'.
+package meta
+
+import (
+ "regexp"
+ "sort"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/input"
+ "t73f.de/r/zsc/maps"
+ "zettelstore.de/z/strfun"
+ "zettelstore.de/z/zettel/id"
+)
+
+type keyUsage int
+
+const (
+ _ keyUsage = iota
+ usageUser // Key will be manipulated by the user
+ usageComputed // Key is computed by zettelstore
+ usageProperty // Key is computed and not stored by zettelstore
+)
+
+// DescriptionKey formally describes each supported metadata key.
+type DescriptionKey struct {
+ Name string
+ Type *DescriptionType
+ usage keyUsage
+ Inverse string
+}
+
+// IsComputed returns true, if metadata is computed and not set by the user.
+func (kd *DescriptionKey) IsComputed() bool { return kd.usage >= usageComputed }
+
+// IsProperty returns true, if metadata is a computed property.
+func (kd *DescriptionKey) IsProperty() bool { return kd.usage >= usageProperty }
+
+var registeredKeys = make(map[string]*DescriptionKey)
+
+func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) {
+ if _, ok := registeredKeys[name]; ok {
+ panic("Key '" + name + "' already defined")
+ }
+ if inverse != "" {
+ if t != TypeID && t != TypeIDSet {
+ panic("Inversable key '" + name + "' is not identifier type, but " + t.String())
+ }
+ inv, ok := registeredKeys[inverse]
+ if !ok {
+ panic("Inverse Key '" + inverse + "' not found")
+ }
+ if !inv.IsComputed() {
+ panic("Inverse Key '" + inverse + "' is not computed.")
+ }
+ if inv.Type != TypeIDSet {
+ panic("Inverse Key '" + inverse + "' is not an identifier set, but " + inv.Type.String())
+ }
+ }
+ registeredKeys[name] = &DescriptionKey{name, t, usage, inverse}
+}
+
+// IsComputed returns true, if key denotes a computed metadata key.
+func IsComputed(name string) bool {
+ if kd, ok := registeredKeys[name]; ok {
+ return kd.IsComputed()
+ }
+ return false
+}
+
+// IsProperty returns true, if key denotes a property metadata value.
+func IsProperty(name string) bool {
+ if kd, ok := registeredKeys[name]; ok {
+ return kd.IsProperty()
+ }
+ return false
+}
+
+// Inverse returns the name of the inverse key.
+func Inverse(name string) string {
+ if kd, ok := registeredKeys[name]; ok {
+ return kd.Inverse
+ }
+ return ""
+}
+
+// GetDescription returns the key description object of the given key name.
+func GetDescription(name string) DescriptionKey {
+ if d, ok := registeredKeys[name]; ok {
+ return *d
+ }
+ return DescriptionKey{Type: Type(name)}
+}
+
+// GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name.
+func GetSortedKeyDescriptions() []*DescriptionKey {
+ keys := maps.Keys(registeredKeys)
+ result := make([]*DescriptionKey, 0, len(keys))
+ for _, n := range keys {
+ result = append(result, registeredKeys[n])
+ }
+ return result
+}
+
+// Supported keys.
+func init() {
+ registerKey(api.KeyID, TypeID, usageComputed, "")
+ registerKey(api.KeyTitle, TypeEmpty, usageUser, "")
+ registerKey(api.KeyRole, TypeWord, usageUser, "")
+ registerKey(api.KeyTags, TypeTagSet, usageUser, "")
+ registerKey(api.KeySyntax, TypeWord, usageUser, "")
+
+ // Properties that are inverse keys
+ registerKey(api.KeyFolge, TypeIDSet, usageProperty, "")
+ registerKey(api.KeySuccessors, TypeIDSet, usageProperty, "")
+ registerKey(api.KeySubordinates, TypeIDSet, usageProperty, "")
+
+ // Non-inverse keys
+ registerKey(api.KeyAuthor, TypeString, usageUser, "")
+ registerKey(api.KeyBack, TypeIDSet, usageProperty, "")
+ registerKey(api.KeyBackward, TypeIDSet, usageProperty, "")
+ registerKey(api.KeyBoxNumber, TypeNumber, usageProperty, "")
+ registerKey(api.KeyCopyright, TypeString, usageUser, "")
+ registerKey(api.KeyCreated, TypeTimestamp, usageComputed, "")
+ registerKey(api.KeyCredential, TypeCredential, usageUser, "")
+ registerKey(api.KeyDead, TypeIDSet, usageProperty, "")
+ registerKey(api.KeyExpire, TypeTimestamp, usageUser, "")
+ registerKey(api.KeyFolgeRole, TypeWord, usageUser, "")
+ registerKey(api.KeyForward, TypeIDSet, usageProperty, "")
+ registerKey(api.KeyLang, TypeWord, usageUser, "")
+ registerKey(api.KeyLicense, TypeEmpty, usageUser, "")
+ registerKey(api.KeyModified, TypeTimestamp, usageComputed, "")
+ registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge)
+ registerKey(api.KeyPredecessor, TypeID, usageUser, api.KeySuccessors)
+ registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "")
+ registerKey(api.KeyQuery, TypeEmpty, usageUser, "")
+ registerKey(api.KeyReadOnly, TypeWord, usageUser, "")
+ registerKey(api.KeySummary, TypeZettelmarkup, usageUser, "")
+ registerKey(api.KeySuperior, TypeIDSet, usageUser, api.KeySubordinates)
+ registerKey(api.KeyURL, TypeURL, usageUser, "")
+ registerKey(api.KeyUselessFiles, TypeString, usageProperty, "")
+ registerKey(api.KeyUserID, TypeWord, usageUser, "")
+ registerKey(api.KeyUserRole, TypeWord, usageUser, "")
+ registerKey(api.KeyVisibility, TypeWord, usageUser, "")
+}
+
+// NewPrefix is the prefix for metadata key in template zettel for creating new zettel.
+const NewPrefix = "new-"
+
+// Meta contains all meta-data of a zettel.
+type Meta struct {
+ Zid id.Zid
+ pairs map[string]string
+ YamlSep bool
+}
+
+// New creates a new chunk for storing metadata.
+func New(zid id.Zid) *Meta {
+ return &Meta{Zid: zid, pairs: make(map[string]string, 5)}
+}
+
+// NewWithData creates metadata object with given data.
+func NewWithData(zid id.Zid, data map[string]string) *Meta {
+ pairs := make(map[string]string, len(data))
+ for k, v := range data {
+ pairs[k] = v
+ }
+ return &Meta{Zid: zid, pairs: pairs}
+}
+
+// Length returns the number of bytes stored for the metadata.
+func (m *Meta) Length() int {
+ if m == nil {
+ return 0
+ }
+ result := 6 // storage needed for Zid
+ for k, v := range m.pairs {
+ result += len(k) + len(v) + 1 // 1 because separator
+ }
+ return result
+}
+
+// Clone returns a new copy of the metadata.
+func (m *Meta) Clone() *Meta {
+ return &Meta{
+ Zid: m.Zid,
+ pairs: m.Map(),
+ YamlSep: m.YamlSep,
+ }
+}
+
+// Map returns a copy of the meta data as a string map.
+func (m *Meta) Map() map[string]string {
+ pairs := make(map[string]string, len(m.pairs))
+ for k, v := range m.pairs {
+ pairs[k] = v
+ }
+ return pairs
+}
+
+var reKey = regexp.MustCompile("^[0-9a-z][-0-9a-z]{0,254}$")
+
+// KeyIsValid returns true, if the string is a valid metadata key.
+func KeyIsValid(s string) bool { return reKey.MatchString(s) }
+
+// Pair is one key-value-pair of a Zettel meta.
+type Pair struct {
+ Key string
+ Value string
+}
+
+var firstKeys = []string{api.KeyTitle, api.KeyRole, api.KeyTags, api.KeySyntax}
+var firstKeySet strfun.Set
+
+func init() {
+ firstKeySet = strfun.NewSet(firstKeys...)
+}
+
+// Set stores the given string value under the given key.
+func (m *Meta) Set(key, value string) {
+ if key != api.KeyID {
+ m.pairs[key] = trimValue(value)
+ }
+}
+
+// SetNonEmpty stores the given value under the given key, if the value is non-empty.
+// An empty value will delete the previous association.
+func (m *Meta) SetNonEmpty(key, value string) {
+ if value == "" {
+ delete(m.pairs, key)
+ } else {
+ m.Set(key, trimValue(value))
+ }
+}
+
+func trimValue(value string) string {
+ return strings.TrimFunc(value, input.IsSpace)
+}
+
+// Get retrieves the string value of a given key. The bool value signals,
+// whether there was a value stored or not.
+func (m *Meta) Get(key string) (string, bool) {
+ if m == nil {
+ return "", false
+ }
+ if key == api.KeyID {
+ return m.Zid.String(), true
+ }
+ value, ok := m.pairs[key]
+ return value, ok
+}
+
+// GetDefault retrieves the string value of the given key. If no value was
+// stored, the given default value is returned.
+func (m *Meta) GetDefault(key, def string) string {
+ if value, found := m.Get(key); found {
+ return value
+ }
+ return def
+}
+
+// GetTitle returns the title of the metadata. It is the only key that has a
+// defined default value: the string representation of the zettel identifier.
+func (m *Meta) GetTitle() string {
+ if title, found := m.Get(api.KeyTitle); found {
+ return title
+ }
+ return m.Zid.String()
+}
+
+// Pairs returns not computed key/values pairs stored, in a specific order.
+// First come the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey,
+// MetaContextKey. Then all other pairs are append to the list, ordered by key.
+func (m *Meta) Pairs() []Pair {
+ return m.doPairs(m.getFirstKeys(), notComputedKey)
+}
+
+// ComputedPairs returns all key/values pairs stored, in a specific order. First come
+// the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey,
+// MetaContextKey. Then all other pairs are append to the list, ordered by key.
+func (m *Meta) ComputedPairs() []Pair {
+ return m.doPairs(m.getFirstKeys(), anyKey)
+}
+
+// PairsRest returns not computed key/values pairs stored, except the values with
+// predefined keys. The pairs are ordered by key.
+func (m *Meta) PairsRest() []Pair {
+ result := make([]Pair, 0, len(m.pairs))
+ return m.doPairs(result, notComputedKey)
+}
+
+// ComputedPairsRest returns all key/values pairs stored, except the values with
+// predefined keys. The pairs are ordered by key.
+func (m *Meta) ComputedPairsRest() []Pair {
+ result := make([]Pair, 0, len(m.pairs))
+ return m.doPairs(result, anyKey)
+}
+
+func notComputedKey(key string) bool { return !IsComputed(key) }
+func anyKey(string) bool { return true }
+
+func (m *Meta) doPairs(firstKeys []Pair, addKeyPred func(string) bool) []Pair {
+ keys := m.getKeysRest(addKeyPred)
+ for _, k := range keys {
+ firstKeys = append(firstKeys, Pair{k, m.pairs[k]})
+ }
+ return firstKeys
+}
+
+func (m *Meta) getFirstKeys() []Pair {
+ result := make([]Pair, 0, len(m.pairs))
+ for _, key := range firstKeys {
+ if value, ok := m.pairs[key]; ok {
+ result = append(result, Pair{key, value})
+ }
+ }
+ return result
+}
+
+func (m *Meta) getKeysRest(addKeyPred func(string) bool) []string {
+ keys := make([]string, 0, len(m.pairs))
+ for k := range m.pairs {
+ if !firstKeySet.Has(k) && addKeyPred(k) {
+ keys = append(keys, k)
+ }
+ }
+ sort.Strings(keys)
+ return keys
+}
+
+// Delete removes a key from the data.
+func (m *Meta) Delete(key string) {
+ if key != api.KeyID {
+ delete(m.pairs, key)
+ }
+}
+
+// Equal compares to metas for equality.
+func (m *Meta) Equal(o *Meta, allowComputed bool) bool {
+ if m == nil && o == nil {
+ return true
+ }
+ if m == nil || o == nil || m.Zid != o.Zid {
+ return false
+ }
+ tested := make(strfun.Set, len(m.pairs))
+ for k, v := range m.pairs {
+ tested.Set(k)
+ if !equalValue(k, v, o, allowComputed) {
+ return false
+ }
+ }
+ for k, v := range o.pairs {
+ if !tested.Has(k) && !equalValue(k, v, m, allowComputed) {
+ return false
+ }
+ }
+ return true
+}
+
+func equalValue(key, val string, other *Meta, allowComputed bool) bool {
+ if allowComputed || !IsComputed(key) {
+ if valO, found := other.pairs[key]; !found || val != valO {
+ return false
+ }
+ }
+ return true
+}
+
+// Sanitize all metadata keys and values, so that they can be written safely into a file.
+func (m *Meta) Sanitize() {
+ if m == nil {
+ return
+ }
+ for k, v := range m.pairs {
+ m.pairs[RemoveNonGraphic(k)] = RemoveNonGraphic(v)
+ }
+}
+
+// RemoveNonGraphic changes the given string not to include non-graphical characters.
+// It is needed to sanitize meta data.
+func RemoveNonGraphic(s string) string {
+ if s == "" {
+ return ""
+ }
+ pos := 0
+ var sb strings.Builder
+ for pos < len(s) {
+ nextPos := strings.IndexFunc(s[pos:], func(r rune) bool { return !unicode.IsGraphic(r) })
+ if nextPos < 0 {
+ break
+ }
+ sb.WriteString(s[pos:nextPos])
+ sb.WriteByte(' ')
+ _, size := utf8.DecodeRuneInString(s[nextPos:])
+ pos = nextPos + size
+ }
+ if pos == 0 {
+ return strings.TrimSpace(s)
+ }
+ sb.WriteString(s[pos:])
+ return strings.TrimSpace(sb.String())
+}
ADDED zettel/meta/meta_test.go
Index: zettel/meta/meta_test.go
==================================================================
--- zettel/meta/meta_test.go
+++ zettel/meta/meta_test.go
@@ -0,0 +1,264 @@
+//-----------------------------------------------------------------------------
+// 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 meta
+
+import (
+ "strings"
+ "testing"
+
+ "t73f.de/r/zsc/api"
+ "zettelstore.de/z/zettel/id"
+)
+
+const testID = id.Zid(98765432101234)
+
+func TestKeyIsValid(t *testing.T) {
+ t.Parallel()
+ validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)}
+ for _, key := range validKeys {
+ if !KeyIsValid(key) {
+ t.Errorf("Key %q wrongly identified as invalid key", key)
+ }
+ }
+ invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)}
+ for _, key := range invalidKeys {
+ if KeyIsValid(key) {
+ t.Errorf("Key %q wrongly identified as valid key", key)
+ }
+ }
+}
+
+func TestTitleHeader(t *testing.T) {
+ t.Parallel()
+ m := New(testID)
+ if got, ok := m.Get(api.KeyTitle); ok && got != "" {
+ t.Errorf("Title is not empty, but %q", got)
+ }
+ addToMeta(m, api.KeyTitle, " ")
+ if got, ok := m.Get(api.KeyTitle); ok && got != "" {
+ t.Errorf("Title is not empty, but %q", got)
+ }
+ const st = "A simple text"
+ addToMeta(m, api.KeyTitle, " "+st+" ")
+ if got, ok := m.Get(api.KeyTitle); !ok || got != st {
+ t.Errorf("Title is not %q, but %q", st, got)
+ }
+ addToMeta(m, api.KeyTitle, " "+st+"\t")
+ const exp = st + " " + st
+ if got, ok := m.Get(api.KeyTitle); !ok || got != exp {
+ t.Errorf("Title is not %q, but %q", exp, got)
+ }
+
+ m = New(testID)
+ const at = "A Title"
+ addToMeta(m, api.KeyTitle, at)
+ addToMeta(m, api.KeyTitle, " ")
+ if got, ok := m.Get(api.KeyTitle); !ok || got != at {
+ t.Errorf("Title is not %q, but %q", at, got)
+ }
+}
+
+func checkTags(t *testing.T, exp []string, m *Meta) {
+ t.Helper()
+ got, _ := m.GetList(api.KeyTags)
+ for i, tag := range exp {
+ if i < len(got) {
+ if tag != got[i] {
+ t.Errorf("Pos=%d, expected %q, got %q", i, exp[i], got[i])
+ }
+ } else {
+ t.Errorf("Expected %q, but is missing", exp[i])
+ }
+ }
+ if len(exp) < len(got) {
+ t.Errorf("Extra tags: %q", got[len(exp):])
+ }
+}
+
+func TestTagsHeader(t *testing.T) {
+ t.Parallel()
+ m := New(testID)
+ checkTags(t, []string{}, m)
+
+ addToMeta(m, api.KeyTags, "")
+ checkTags(t, []string{}, m)
+
+ addToMeta(m, api.KeyTags, " #t1 #t2 #t3 #t4 ")
+ checkTags(t, []string{"#t1", "#t2", "#t3", "#t4"}, m)
+
+ addToMeta(m, api.KeyTags, "#t5")
+ checkTags(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m)
+
+ addToMeta(m, api.KeyTags, "t6")
+ checkTags(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m)
+}
+
+func TestSyntax(t *testing.T) {
+ t.Parallel()
+ m := New(testID)
+ if got, ok := m.Get(api.KeySyntax); ok || got != "" {
+ t.Errorf("Syntax is not %q, but %q", "", got)
+ }
+ addToMeta(m, api.KeySyntax, " ")
+ if got, _ := m.Get(api.KeySyntax); got != "" {
+ t.Errorf("Syntax is not %q, but %q", "", got)
+ }
+ addToMeta(m, api.KeySyntax, "MarkDown")
+ const exp = "markdown"
+ if got, ok := m.Get(api.KeySyntax); !ok || got != exp {
+ t.Errorf("Syntax is not %q, but %q", exp, got)
+ }
+ addToMeta(m, api.KeySyntax, " ")
+ if got, _ := m.Get(api.KeySyntax); got != "" {
+ t.Errorf("Syntax is not %q, but %q", "", got)
+ }
+}
+
+func checkHeader(t *testing.T, exp map[string]string, gotP []Pair) {
+ t.Helper()
+ got := make(map[string]string, len(gotP))
+ for _, p := range gotP {
+ got[p.Key] = p.Value
+ if _, ok := exp[p.Key]; !ok {
+ t.Errorf("Key %q is not expected, but has value %q", p.Key, p.Value)
+ }
+ }
+ for k, v := range exp {
+ if gv, ok := got[k]; !ok || v != gv {
+ if ok {
+ t.Errorf("Key %q is not %q, but %q", k, v, got[k])
+ } else {
+ t.Errorf("Key %q missing, should have value %q", k, v)
+ }
+ }
+ }
+}
+
+func TestDefaultHeader(t *testing.T) {
+ t.Parallel()
+ m := New(testID)
+ addToMeta(m, "h1", "d1")
+ addToMeta(m, "H2", "D2")
+ addToMeta(m, "H1", "D1.1")
+ exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"}
+ checkHeader(t, exp, m.Pairs())
+ addToMeta(m, "", "d0")
+ checkHeader(t, exp, m.Pairs())
+ addToMeta(m, "h3", "")
+ exp["h3"] = ""
+ checkHeader(t, exp, m.Pairs())
+ addToMeta(m, "h3", " ")
+ checkHeader(t, exp, m.Pairs())
+ addToMeta(m, "h4", " ")
+ exp["h4"] = ""
+ checkHeader(t, exp, m.Pairs())
+}
+
+func TestDelete(t *testing.T) {
+ t.Parallel()
+ m := New(testID)
+ m.Set("key", "val")
+ if got, ok := m.Get("key"); !ok || got != "val" {
+ t.Errorf("Value != %q, got: %v/%q", "val", ok, got)
+ }
+ m.Set("key", "")
+ if got, ok := m.Get("key"); !ok || got != "" {
+ t.Errorf("Value != %q, got: %v/%q", "", ok, got)
+ }
+ m.Delete("key")
+ if got, ok := m.Get("key"); ok || got != "" {
+ t.Errorf("Value != %q, got: %v/%q", "", ok, got)
+ }
+}
+
+func TestEqual(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ pairs1, pairs2 []string
+ allowComputed bool
+ exp bool
+ }{
+ {nil, nil, true, true},
+ {nil, nil, false, true},
+ {[]string{"a", "a"}, nil, false, false},
+ {[]string{"a", "a"}, nil, true, false},
+ {[]string{api.KeyFolge, "0"}, nil, true, false},
+ {[]string{api.KeyFolge, "0"}, nil, false, true},
+ {[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, true, true},
+ {[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, false, true},
+ }
+ for i, tc := range testcases {
+ m1 := pairs2meta(tc.pairs1)
+ m2 := pairs2meta(tc.pairs2)
+ got := m1.Equal(m2, tc.allowComputed)
+ if tc.exp != got {
+ t.Errorf("%d: %v =?= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got)
+ }
+ got = m2.Equal(m1, tc.allowComputed)
+ if tc.exp != got {
+ t.Errorf("%d: %v =!= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got)
+ }
+ }
+
+ // Pathologic cases
+ var m1, m2 *Meta
+ if !m1.Equal(m2, true) {
+ t.Error("Nil metas should be treated equal")
+ }
+ m1 = New(testID)
+ if m1.Equal(m2, true) {
+ t.Error("Empty meta should not be equal to nil")
+ }
+ if m2.Equal(m1, true) {
+ t.Error("Nil meta should should not be equal to empty")
+ }
+ m2 = New(testID + 1)
+ if m1.Equal(m2, true) {
+ t.Error("Different ID should differentiate")
+ }
+ if m2.Equal(m1, true) {
+ t.Error("Different ID should differentiate")
+ }
+}
+
+func pairs2meta(pairs []string) *Meta {
+ m := New(testID)
+ for i := 0; i < len(pairs); i += 2 {
+ m.Set(pairs[i], pairs[i+1])
+ }
+ return m
+}
+
+func TestRemoveNonGraphic(t *testing.T) {
+ testCases := []struct {
+ inp string
+ exp string
+ }{
+ {"", ""},
+ {" ", ""},
+ {"a", "a"},
+ {"a ", "a"},
+ {"a b", "a b"},
+ {"\n", ""},
+ {"a\n", "a"},
+ {"a\nb", "a b"},
+ {"a\tb", "a b"},
+ }
+ for i, tc := range testCases {
+ got := RemoveNonGraphic(tc.inp)
+ if tc.exp != got {
+ t.Errorf("%q/%d: expected %q, but got %q", tc.inp, i, tc.exp, got)
+ }
+ }
+}
ADDED zettel/meta/parse.go
Index: zettel/meta/parse.go
==================================================================
--- zettel/meta/parse.go
+++ zettel/meta/parse.go
@@ -0,0 +1,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 meta
+
+import (
+ "strings"
+
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/input"
+ "t73f.de/r/zsc/maps"
+ "zettelstore.de/z/strfun"
+ "zettelstore.de/z/zettel/id"
+)
+
+// NewFromInput parses the meta data of a zettel.
+func NewFromInput(zid id.Zid, inp *input.Input) *Meta {
+ if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' {
+ skipToEOL(inp)
+ inp.EatEOL()
+ }
+ meta := New(zid)
+ for {
+ skipSpace(inp)
+ switch inp.Ch {
+ case '\r':
+ if inp.Peek() == '\n' {
+ inp.Next()
+ }
+ fallthrough
+ case '\n':
+ inp.Next()
+ return meta
+ case input.EOS:
+ return meta
+ case '%':
+ skipToEOL(inp)
+ inp.EatEOL()
+ continue
+ }
+ parseHeader(meta, inp)
+ if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' {
+ skipToEOL(inp)
+ inp.EatEOL()
+ meta.YamlSep = true
+ return meta
+ }
+ }
+}
+
+func parseHeader(m *Meta, inp *input.Input) {
+ pos := inp.Pos
+ for isHeader(inp.Ch) {
+ inp.Next()
+ }
+ key := inp.Src[pos:inp.Pos]
+ skipSpace(inp)
+ if inp.Ch == ':' {
+ inp.Next()
+ }
+ var val []byte
+ for {
+ skipSpace(inp)
+ pos = inp.Pos
+ skipToEOL(inp)
+ val = append(val, inp.Src[pos:inp.Pos]...)
+ inp.EatEOL()
+ if !input.IsSpace(inp.Ch) {
+ break
+ }
+ val = append(val, ' ')
+ }
+ addToMeta(m, string(key), string(val))
+}
+
+func skipSpace(inp *input.Input) {
+ for input.IsSpace(inp.Ch) {
+ inp.Next()
+ }
+}
+
+func skipToEOL(inp *input.Input) {
+ for {
+ switch inp.Ch {
+ case '\n', '\r', input.EOS:
+ return
+ }
+ inp.Next()
+ }
+}
+
+// Return true iff rune is valid for header key.
+func isHeader(ch rune) bool {
+ return ('a' <= ch && ch <= 'z') ||
+ ('0' <= ch && ch <= '9') ||
+ ch == '-' ||
+ ('A' <= ch && ch <= 'Z')
+}
+
+type predValidElem func(string) bool
+
+func addToSet(set strfun.Set, elems []string, useElem predValidElem) {
+ for _, s := range elems {
+ if len(s) > 0 && useElem(s) {
+ set.Set(s)
+ }
+ }
+}
+
+func addSet(m *Meta, key, val string, useElem predValidElem) {
+ newElems := strings.Fields(val)
+ oldElems, ok := m.GetList(key)
+ if !ok {
+ oldElems = nil
+ }
+
+ set := make(strfun.Set, len(newElems)+len(oldElems))
+ addToSet(set, newElems, useElem)
+ if len(set) == 0 {
+ // Nothing to add. Maybe because of rejected elements.
+ return
+ }
+ addToSet(set, oldElems, useElem)
+ m.SetList(key, maps.Keys(set))
+}
+
+func addData(m *Meta, k, v string) {
+ if o, ok := m.Get(k); !ok || o == "" {
+ m.Set(k, v)
+ } else if v != "" {
+ m.Set(k, o+" "+v)
+ }
+}
+
+func addToMeta(m *Meta, key, val string) {
+ v := trimValue(val)
+ key = strings.ToLower(key)
+ if !KeyIsValid(key) {
+ return
+ }
+ switch key {
+ case "", api.KeyID:
+ // Empty key and 'id' key will be ignored
+ return
+ }
+
+ switch Type(key) {
+ case TypeTagSet:
+ addSet(m, key, strings.ToLower(v), func(s string) bool { return s[0] == '#' && len(s) > 1 })
+ case TypeWord:
+ m.Set(key, strings.ToLower(v))
+ case TypeID:
+ if _, err := id.Parse(v); err == nil {
+ m.Set(key, v)
+ }
+ case TypeIDSet:
+ addSet(m, key, v, func(s string) bool {
+ _, err := id.Parse(s)
+ return err == nil
+ })
+ case TypeTimestamp:
+ if _, ok := TimeValue(v); ok {
+ m.Set(key, v)
+ }
+ default:
+ addData(m, key, v)
+ }
+}
ADDED zettel/meta/parse_test.go
Index: zettel/meta/parse_test.go
==================================================================
--- zettel/meta/parse_test.go
+++ zettel/meta/parse_test.go
@@ -0,0 +1,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 meta_test
+
+import (
+ "strings"
+ "testing"
+
+ "t73f.de/r/zsc/api"
+ "t73f.de/r/zsc/input"
+ "zettelstore.de/z/zettel/meta"
+)
+
+func parseMetaStr(src string) *meta.Meta {
+ return meta.NewFromInput(testID, input.NewInput([]byte(src)))
+}
+
+func TestEmpty(t *testing.T) {
+ t.Parallel()
+ m := parseMetaStr("")
+ if got, ok := m.Get(api.KeySyntax); ok || got != "" {
+ t.Errorf("Syntax is not %q, but %q", "", got)
+ }
+ if got, ok := m.GetList(api.KeyTags); ok || len(got) > 0 {
+ t.Errorf("Tags are not nil, but %v", got)
+ }
+}
+
+func TestTitle(t *testing.T) {
+ t.Parallel()
+ td := []struct{ s, e string }{
+ {api.KeyTitle + ": a title", "a title"},
+ {api.KeyTitle + ": a\n\t title", "a title"},
+ {api.KeyTitle + ": a\n\t title\r\n x", "a title x"},
+ {api.KeyTitle + " AbC", "AbC"},
+ {api.KeyTitle + " AbC\n ded", "AbC ded"},
+ {api.KeyTitle + ": o\ntitle: p", "o p"},
+ {api.KeyTitle + ": O\n\ntitle: P", "O"},
+ {api.KeyTitle + ": b\r\ntitle: c", "b c"},
+ {api.KeyTitle + ": B\r\n\r\ntitle: C", "B"},
+ {api.KeyTitle + ": r\rtitle: q", "r q"},
+ {api.KeyTitle + ": R\r\rtitle: Q", "R"},
+ }
+ for i, tc := range td {
+ m := parseMetaStr(tc.s)
+ if got, ok := m.Get(api.KeyTitle); !ok || got != tc.e {
+ t.Log(m)
+ t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got)
+ }
+ }
+}
+
+func TestTags(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ src string
+ exp string
+ }{
+ {"", ""},
+ {api.KeyTags + ":", ""},
+ {api.KeyTags + ": c", ""},
+ {api.KeyTags + ": #", ""},
+ {api.KeyTags + ": #c", "c"},
+ {api.KeyTags + ": #c #", "c"},
+ {api.KeyTags + ": #c #b", "b c"},
+ {api.KeyTags + ": #c # #", "c"},
+ {api.KeyTags + ": #c # #b", "b c"},
+ }
+ for i, tc := range testcases {
+ m := parseMetaStr(tc.src)
+ tagsString, found := m.Get(api.KeyTags)
+ if !found {
+ if tc.exp != "" {
+ t.Errorf("%d / %q: no %s found", i, tc.src, api.KeyTags)
+ }
+ continue
+ }
+ tags := meta.TagsFromValue(tagsString)
+ if tc.exp == "" && len(tags) > 0 {
+ t.Errorf("%d / %q: expected no %s, but got %v", i, tc.src, api.KeyTags, tags)
+ continue
+ }
+ got := strings.Join(tags, " ")
+ if tc.exp != got {
+ t.Errorf("%d / %q: expected %q, got: %q", i, tc.src, tc.exp, got)
+ }
+ }
+}
+
+func TestNewFromInput(t *testing.T) {
+ t.Parallel()
+ testcases := []struct {
+ input string
+ exp []meta.Pair
+ }{
+ {"", []meta.Pair{}},
+ {" a:b", []meta.Pair{{"a", "b"}}},
+ {"%a:b", []meta.Pair{}},
+ {"a:b\r\n\r\nc:d", []meta.Pair{{"a", "b"}}},
+ {"a:b\r\n%c:d", []meta.Pair{{"a", "b"}}},
+ {"% a:b\r\n c:d", []meta.Pair{{"c", "d"}}},
+ {"---\r\na:b\r\n", []meta.Pair{{"a", "b"}}},
+ {"---\r\na:b\r\n--\r\nc:d", []meta.Pair{{"a", "b"}, {"c", "d"}}},
+ {"---\r\na:b\r\n---\r\nc:d", []meta.Pair{{"a", "b"}}},
+ {"---\r\na:b\r\n----\r\nc:d", []meta.Pair{{"a", "b"}}},
+ {"new-title:\nnew-url:", []meta.Pair{{"new-title", ""}, {"new-url", ""}}},
+ }
+ for i, tc := range testcases {
+ meta := parseMetaStr(tc.input)
+ if got := meta.Pairs(); !equalPairs(tc.exp, got) {
+ t.Errorf("TC=%d: expected=%v, got=%v", i, tc.exp, got)
+ }
+ }
+
+ // Test, whether input position is correct.
+ inp := input.NewInput([]byte("---\na:b\n---\nX"))
+ m := meta.NewFromInput(testID, inp)
+ exp := []meta.Pair{{"a", "b"}}
+ if got := m.Pairs(); !equalPairs(exp, got) {
+ t.Errorf("Expected=%v, got=%v", exp, got)
+ }
+ expCh := 'X'
+ if gotCh := inp.Ch; gotCh != expCh {
+ t.Errorf("Expected=%v, got=%v", expCh, gotCh)
+ }
+}
+
+func equalPairs(one, two []meta.Pair) bool {
+ if len(one) != len(two) {
+ return false
+ }
+ for i := range len(one) {
+ if one[i].Key != two[i].Key || one[i].Value != two[i].Value {
+ return false
+ }
+ }
+ return true
+}
+
+func TestPrecursorIDSet(t *testing.T) {
+ t.Parallel()
+ var testdata = []struct {
+ inp string
+ exp string
+ }{
+ {"", ""},
+ {"123", ""},
+ {"12345678901234", "12345678901234"},
+ {"123 12345678901234", "12345678901234"},
+ {"12345678901234 123", "12345678901234"},
+ {"01234567890123 123 12345678901234", "01234567890123 12345678901234"},
+ {"12345678901234 01234567890123", "01234567890123 12345678901234"},
+ }
+ for i, tc := range testdata {
+ m := parseMetaStr(api.KeyPrecursor + ": " + tc.inp)
+ if got, ok := m.Get(api.KeyPrecursor); (!ok && tc.exp != "") || tc.exp != got {
+ t.Errorf("TC=%d: expected %q, but got %q when parsing %q", i, tc.exp, got, tc.inp)
+ }
+ }
+}
ADDED zettel/meta/type.go
Index: zettel/meta/type.go
==================================================================
--- zettel/meta/type.go
+++ zettel/meta/type.go
@@ -0,0 +1,230 @@
+//-----------------------------------------------------------------------------
+// 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 meta
+
+import (
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "t73f.de/r/zsc/api"
+ "zettelstore.de/z/zettel/id"
+)
+
+// DescriptionType is a description of a specific key type.
+type DescriptionType struct {
+ Name string
+ IsSet bool
+}
+
+// String returns the string representation of the given type
+func (t DescriptionType) String() string { return t.Name }
+
+var registeredTypes = make(map[string]*DescriptionType)
+
+func registerType(name string, isSet bool) *DescriptionType {
+ if _, ok := registeredTypes[name]; ok {
+ panic("Type '" + name + "' already registered")
+ }
+ t := &DescriptionType{name, isSet}
+ registeredTypes[name] = t
+ return t
+}
+
+// Supported key types.
+var (
+ TypeCredential = registerType(api.MetaCredential, false)
+ TypeEmpty = registerType(api.MetaEmpty, false)
+ TypeID = registerType(api.MetaID, false)
+ TypeIDSet = registerType(api.MetaIDSet, true)
+ TypeNumber = registerType(api.MetaNumber, false)
+ TypeString = registerType(api.MetaString, false)
+ TypeTagSet = registerType(api.MetaTagSet, true)
+ TypeTimestamp = registerType(api.MetaTimestamp, false)
+ TypeURL = registerType(api.MetaURL, false)
+ TypeWord = registerType(api.MetaWord, false)
+ TypeZettelmarkup = registerType(api.MetaZettelmarkup, false)
+)
+
+// Type returns a type hint for the given key. If no type hint is specified,
+// TypeUnknown is returned.
+func (*Meta) Type(key string) *DescriptionType {
+ return Type(key)
+}
+
+// Some constants for key suffixes that determine a type.
+const (
+ SuffixKeyRole = "-role"
+ SuffixKeyURL = "-url"
+)
+
+var (
+ cachedTypedKeys = make(map[string]*DescriptionType)
+ mxTypedKey sync.RWMutex
+ suffixTypes = map[string]*DescriptionType{
+ "-date": TypeTimestamp,
+ "-number": TypeNumber,
+ SuffixKeyRole: TypeWord,
+ "-time": TypeTimestamp,
+ "-title": TypeZettelmarkup,
+ SuffixKeyURL: TypeURL,
+ "-zettel": TypeID,
+ "-zid": TypeID,
+ "-zids": TypeIDSet,
+ }
+)
+
+// Type returns a type hint for the given key. If no type hint is specified,
+// TypeEmpty is returned.
+func Type(key string) *DescriptionType {
+ if k, ok := registeredKeys[key]; ok {
+ return k.Type
+ }
+ mxTypedKey.RLock()
+ k, ok := cachedTypedKeys[key]
+ mxTypedKey.RUnlock()
+ if ok {
+ return k
+ }
+ for suffix, t := range suffixTypes {
+ if strings.HasSuffix(key, suffix) {
+ mxTypedKey.Lock()
+ defer mxTypedKey.Unlock()
+ cachedTypedKeys[key] = t
+ return t
+ }
+ }
+ return TypeEmpty
+}
+
+// SetList stores the given string list value under the given key.
+func (m *Meta) SetList(key string, values []string) {
+ if key != api.KeyID {
+ for i, val := range values {
+ values[i] = trimValue(val)
+ }
+ m.pairs[key] = strings.Join(values, " ")
+ }
+}
+
+// SetWord stores the given word under the given key.
+func (m *Meta) SetWord(key, word string) {
+ if slist := ListFromValue(word); len(slist) > 0 {
+ m.Set(key, slist[0])
+ }
+}
+
+// SetNow stores the current timestamp under the given key.
+func (m *Meta) SetNow(key string) {
+ m.Set(key, time.Now().Local().Format(id.TimestampLayout))
+}
+
+// BoolValue returns the value interpreted as a bool.
+func BoolValue(value string) bool {
+ if len(value) > 0 {
+ switch value[0] {
+ case '0', 'f', 'F', 'n', 'N':
+ return false
+ }
+ }
+ return true
+}
+
+// GetBool returns the boolean value of the given key.
+func (m *Meta) GetBool(key string) bool {
+ if value, ok := m.Get(key); ok {
+ return BoolValue(value)
+ }
+ return false
+}
+
+// TimeValue returns the time value of the given value.
+func TimeValue(value string) (time.Time, bool) {
+ if t, err := time.Parse(id.TimestampLayout, ExpandTimestamp(value)); err == nil {
+ return t, true
+ }
+ return time.Time{}, false
+}
+
+// ExpandTimestamp makes a short-form timestamp larger.
+func ExpandTimestamp(value string) string {
+ switch l := len(value); l {
+ case 4: // YYYY
+ return value + "0101000000"
+ case 6: // YYYYMM
+ return value + "01000000"
+ case 8, 10, 12: // YYYYMMDD, YYYYMMDDhh, YYYYMMDDhhmm
+ return value + "000000"[:14-l]
+ case 14: // YYYYMMDDhhmmss
+ return value
+ default:
+ if l > 14 {
+ return value[:14]
+ }
+ return value
+ }
+}
+
+// ListFromValue transforms a string value into a list value.
+func ListFromValue(value string) []string {
+ return strings.Fields(value)
+}
+
+// GetList retrieves the string list value of a given key. The bool value
+// signals, whether there was a value stored or not.
+func (m *Meta) GetList(key string) ([]string, bool) {
+ value, ok := m.Get(key)
+ if !ok {
+ return nil, false
+ }
+ return ListFromValue(value), true
+}
+
+// TagsFromValue returns the value as a sequence of normalized tags.
+func TagsFromValue(value string) []string {
+ tags := ListFromValue(strings.ToLower(value))
+ for i, tag := range tags {
+ if len(tag) > 1 && tag[0] == '#' {
+ tags[i] = tag[1:]
+ }
+ }
+ return tags
+}
+
+// CleanTag removes the number character ('#') from a tag value and lowercases it.
+func CleanTag(tag string) string {
+ if len(tag) > 1 && tag[0] == '#' {
+ return tag[1:]
+ }
+ return tag
+}
+
+// NormalizeTag adds a missing prefix "#" to the tag
+func NormalizeTag(tag string) string {
+ if len(tag) > 0 && tag[0] == '#' {
+ return tag
+ }
+ return "#" + tag
+}
+
+// GetNumber retrieves the numeric value of a given key.
+func (m *Meta) GetNumber(key string, def int64) int64 {
+ if value, ok := m.Get(key); ok {
+ if num, err := strconv.ParseInt(value, 10, 64); err == nil {
+ return num
+ }
+ }
+ return def
+}
ADDED zettel/meta/type_test.go
Index: zettel/meta/type_test.go
==================================================================
--- zettel/meta/type_test.go
+++ zettel/meta/type_test.go
@@ -0,0 +1,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 meta_test
+
+import (
+ "strconv"
+ "testing"
+ "time"
+
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
+
+func TestNow(t *testing.T) {
+ t.Parallel()
+ m := meta.New(id.Invalid)
+ m.SetNow("key")
+ val, ok := m.Get("key")
+ if !ok {
+ t.Error("Unable to get value of key")
+ }
+ if len(val) != 14 {
+ t.Errorf("Value is not 14 digits long: %q", val)
+ }
+ if _, err := strconv.ParseInt(val, 10, 64); err != nil {
+ t.Errorf("Unable to parse %q as an int64: %v", val, err)
+ }
+ if _, ok = meta.TimeValue(val); !ok {
+ t.Errorf("Unable to get time from value %q", val)
+ }
+}
+
+func TestTimeValue(t *testing.T) {
+ t.Parallel()
+ testCases := []struct {
+ value string
+ valid bool
+ exp time.Time
+ }{
+ {"", false, time.Time{}},
+ {"1", false, time.Time{}},
+ {"00000000000000", false, time.Time{}},
+ {"98765432109876", false, time.Time{}},
+ {"20201221111905", true, time.Date(2020, time.December, 21, 11, 19, 5, 0, time.UTC)},
+ {"2023", true, time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC)},
+ {"20231", false, time.Time{}},
+ {"202310", true, time.Date(2023, time.October, 1, 0, 0, 0, 0, time.UTC)},
+ {"2023103", false, time.Time{}},
+ {"20231030", true, time.Date(2023, time.October, 30, 0, 0, 0, 0, time.UTC)},
+ {"202310301", false, time.Time{}},
+ {"2023103016", true, time.Date(2023, time.October, 30, 16, 0, 0, 0, time.UTC)},
+ {"20231030165", false, time.Time{}},
+ {"202310301654", true, time.Date(2023, time.October, 30, 16, 54, 0, 0, time.UTC)},
+ {"2023103016541", false, time.Time{}},
+ {"20231030165417", true, time.Date(2023, time.October, 30, 16, 54, 17, 0, time.UTC)},
+ {"2023103916541700", false, time.Time{}},
+ }
+ for i, tc := range testCases {
+ got, ok := meta.TimeValue(tc.value)
+ if ok != tc.valid {
+ t.Errorf("%d: parsing of %q should be %v, but got %v", i, tc.value, tc.valid, ok)
+ continue
+ }
+ if got != tc.exp {
+ t.Errorf("%d: parsing of %q should return %v, but got %v", i, tc.value, tc.exp, got)
+ }
+ }
+}
ADDED zettel/meta/values.go
Index: zettel/meta/values.go
==================================================================
--- zettel/meta/values.go
+++ zettel/meta/values.go
@@ -0,0 +1,115 @@
+//-----------------------------------------------------------------------------
+// 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 meta
+
+import (
+ "fmt"
+
+ "t73f.de/r/zsc/api"
+)
+
+// Supported syntax values.
+const (
+ SyntaxCSS = api.ValueSyntaxCSS
+ SyntaxDraw = api.ValueSyntaxDraw
+ SyntaxGif = api.ValueSyntaxGif
+ SyntaxHTML = api.ValueSyntaxHTML
+ SyntaxJPEG = "jpeg"
+ SyntaxJPG = "jpg"
+ SyntaxMarkdown = api.ValueSyntaxMarkdown
+ SyntaxMD = api.ValueSyntaxMD
+ SyntaxNone = api.ValueSyntaxNone
+ SyntaxPlain = "plain"
+ SyntaxPNG = "png"
+ SyntaxSVG = api.ValueSyntaxSVG
+ SyntaxSxn = api.ValueSyntaxSxn
+ SyntaxText = api.ValueSyntaxText
+ SyntaxTxt = "txt"
+ SyntaxWebp = "webp"
+ SyntaxZmk = api.ValueSyntaxZmk
+
+ DefaultSyntax = SyntaxPlain
+)
+
+// Visibility enumerates the variations of the 'visibility' meta key.
+type Visibility int
+
+// Supported values for visibility.
+const (
+ _ Visibility = iota
+ VisibilityUnknown
+ VisibilityPublic
+ VisibilityCreator
+ VisibilityLogin
+ VisibilityOwner
+ VisibilityExpert
+)
+
+var visMap = map[string]Visibility{
+ api.ValueVisibilityPublic: VisibilityPublic,
+ api.ValueVisibilityCreator: VisibilityCreator,
+ api.ValueVisibilityLogin: VisibilityLogin,
+ api.ValueVisibilityOwner: VisibilityOwner,
+ api.ValueVisibilityExpert: VisibilityExpert,
+}
+var revVisMap = map[Visibility]string{}
+
+func init() {
+ for k, v := range visMap {
+ revVisMap[v] = k
+ }
+}
+
+// GetVisibility returns the visibility value of the given string
+func GetVisibility(val string) Visibility {
+ if vis, ok := visMap[val]; ok {
+ return vis
+ }
+ return VisibilityUnknown
+}
+
+func (v Visibility) String() string {
+ if s, ok := revVisMap[v]; ok {
+ return s
+ }
+ return fmt.Sprintf("Unknown (%d)", v)
+}
+
+// UserRole enumerates the supported values of meta key 'user-role'.
+type UserRole int
+
+// Supported values for user roles.
+const (
+ _ UserRole = iota
+ UserRoleUnknown
+ UserRoleCreator
+ UserRoleReader
+ UserRoleWriter
+ UserRoleOwner
+)
+
+var urMap = map[string]UserRole{
+ api.ValueUserRoleCreator: UserRoleCreator,
+ api.ValueUserRoleReader: UserRoleReader,
+ api.ValueUserRoleWriter: UserRoleWriter,
+ api.ValueUserRoleOwner: UserRoleOwner,
+}
+
+// GetUserRole role returns the user role of the given string.
+func GetUserRole(val string) UserRole {
+ if ur, ok := urMap[val]; ok {
+ return ur
+ }
+ return UserRoleUnknown
+}
ADDED zettel/meta/write.go
Index: zettel/meta/write.go
==================================================================
--- zettel/meta/write.go
+++ zettel/meta/write.go
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------------
+// 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 meta
+
+import "io"
+
+// Write writes metadata to a writer, excluding computed and propery values.
+func (m *Meta) Write(w io.Writer) (int, error) {
+ return m.doWrite(w, IsComputed)
+}
+
+// WriteComputed writes metadata to a writer, including computed values,
+// but excluding property values.
+func (m *Meta) WriteComputed(w io.Writer) (int, error) {
+ return m.doWrite(w, IsProperty)
+}
+
+func (m *Meta) doWrite(w io.Writer, ignoreKeyPred func(string) bool) (length int, err error) {
+ for _, p := range m.ComputedPairs() {
+ key := p.Key
+ if ignoreKeyPred(key) {
+ continue
+ }
+ if err != nil {
+ break
+ }
+ var l int
+ l, err = io.WriteString(w, key)
+ length += l
+ if err == nil {
+ l, err = w.Write(colonSpace)
+ length += l
+ }
+ if err == nil {
+ l, err = io.WriteString(w, p.Value)
+ length += l
+ }
+ if err == nil {
+ l, err = w.Write(newline)
+ length += l
+ }
+ }
+ return length, err
+}
+
+var (
+ colonSpace = []byte{':', ' '}
+ newline = []byte{'\n'}
+)
ADDED zettel/meta/write_test.go
Index: zettel/meta/write_test.go
==================================================================
--- zettel/meta/write_test.go
+++ zettel/meta/write_test.go
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------------
+// 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 meta_test
+
+import (
+ "strings"
+ "testing"
+
+ "t73f.de/r/zsc/api"
+ "zettelstore.de/z/zettel/id"
+ "zettelstore.de/z/zettel/meta"
+)
+
+const testID = id.Zid(98765432101234)
+
+func newMeta(title string, tags []string, syntax string) *meta.Meta {
+ m := meta.New(testID)
+ if title != "" {
+ m.Set(api.KeyTitle, title)
+ }
+ if tags != nil {
+ m.Set(api.KeyTags, strings.Join(tags, " "))
+ }
+ if syntax != "" {
+ m.Set(api.KeySyntax, syntax)
+ }
+ return m
+}
+func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) {
+ t.Helper()
+ var sb strings.Builder
+ m.Write(&sb)
+ if got := sb.String(); got != expected {
+ t.Errorf("\nExp: %q\ngot: %q", expected, got)
+ }
+}
+
+func TestWriteMeta(t *testing.T) {
+ t.Parallel()
+ assertWriteMeta(t, newMeta("", nil, ""), "")
+
+ m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax")
+ assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n")
+
+ m = newMeta("TITLE", nil, "")
+ m.Set("user", "zettel")
+ m.Set("auth", "basic")
+ assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n")
+}
ADDED zettel/zettel.go
Index: zettel/zettel.go
==================================================================
--- zettel/zettel.go
+++ zettel/zettel.go
@@ -0,0 +1,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 "zettelstore.de/z/zettel/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.
+}
+
+// Length returns the number of bytes to store the zettel (in a zettel view,
+// not in a technical view).
+func (z Zettel) Length() int { return z.Meta.Length() + 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)
+}