Zettelstore

Check-in Differences
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Difference From version-0.0.9 To version-0.0.10

2021-02-26
11:53
Increase version to 0.0.11-dev to begin next development cycle ... (check-in: d69e61d8eb user: stern tags: trunk)
11:14
Version 0.0.10 ... (check-in: 5d9e4fc19e user: stern tags: trunk, release, version-0.0.10)
10:36
WebUI: make meta line a little bit darker for better visibility ... (check-in: 09837eae5e user: stern tags: trunk)
2021-01-29
18:44
Typo ... (check-in: 84effdca96 user: stern tags: trunk)
18:16
Version 0.0.9 ... (check-in: 5d25b46c82 user: stern tags: trunk, release, version-0.0.9)
17:34
Prepare for release. Fix indexer bug. Add index.Store.Write. ... (check-in: 2b8648602f user: stern tags: trunk)

Added .deepsource.toml.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
version = 1

[[analyzers]]
name = "go"
enabled = true

  [analyzers.meta]
import_paths = ["github.com/zettelstore/zettelstore"]

Changes to Makefile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46


## Copyright (c) 2020 Detlef Stern
##
## This file is part of zettelstore.
##
## Zettelstore is licensed under the latest version of the EUPL (European Union
## Public License). Please see file LICENSE.txt for your rights and obligations
## under this license.

.PHONY: test check validate race run build build-dev release clean

PACKAGE := zettelstore.de/z/cmd/zettelstore

GO_LDFLAG_VERSION := -X main.buildVersion=$(shell go run tools/version.go || echo unknown)
GOFLAGS_DEVELOP := -ldflags "$(GO_LDFLAG_VERSION)" -tags osusergo,netgo
GOFLAGS_RELEASE := -ldflags "$(GO_LDFLAG_VERSION) -w" -tags osusergo,netgo

test:
	go test ./...

check:
	go vet ./...
	~/go/bin/golint ./...

validate: test check

race:
	go test -race ./...

build-dev:
	mkdir -p bin
	go build $(GOFLAGS_DEVELOP) -o bin/zettelstore $(PACKAGE)

build:
	mkdir -p bin
	go build $(GOFLAGS_RELEASE) -o bin/zettelstore $(PACKAGE)

release:
	mkdir -p releases
	GOARCH=amd64 GOOS=linux go build $(GOFLAGS_RELEASE) -o releases/zettelstore $(PACKAGE)
	GOARCH=arm GOARM=6 GOOS=linux go build $(GOFLAGS_RELEASE) -o releases/zettelstore-arm6 $(PACKAGE)
	GOARCH=amd64 GOOS=darwin go build $(GOFLAGS_RELEASE) -o releases/iZettelstore $(PACKAGE)
	GOARCH=amd64 GOOS=windows go build $(GOFLAGS_RELEASE) -o releases/zettelstore.exe $(PACKAGE)

clean:
	rm -rf bin releases


|







<
|
<

<
<
<
<
<
<
<

<
<
|
<

<
<
|
|
<
<


<
|


|
<
<
<
<


<
>
1
2
3
4
5
6
7
8
9

10

11







12


13

14


15
16


17
18

19
20
21
22




23
24

25

## 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.


.PHONY:  check build release clean









check:


	go run tools/build.go check




version:
	@echo $(shell go run tools/build.go version)



build:

	go run tools/build.go build

release:
	go run tools/build.go release





clean:

	go run tools/build.go clean

Changes to VERSION.

1
0.0.9
|
1
0.0.10

Changes to ast/ast.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
78
79
80
81
82
83
84
85
86
87
88
89

90
91
92
}

// RefState indicates the state of the reference.
type RefState int

// Constants for RefState
const (
	RefStateInvalid      RefState = iota // Invalid URL
	RefStateZettel                       // Valid reference to an internal zettel
	RefStateZettelSelf                   // Valid reference to same zettel with a fragment
	RefStateZettelFound                  // Valid reference to an existing internal zettel
	RefStateZettelBroken                 // Valid reference to a non-existing internal zettel

	RefStateLocal                        // Valid reference to a non-zettel, but local hosted
	RefStateExternal                     // Valid reference to external material
)







|
|
|
|
|
>
|
|

78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
}

// RefState indicates the state of the reference.
type RefState int

// Constants for RefState
const (
	RefStateInvalid  RefState = iota // Invalid Referende
	RefStateZettel                   // Reference to an internal zettel
	RefStateSelf                     // Reference to same zettel with a fragment
	RefStateFound                    // Reference to an existing internal zettel
	RefStateBroken                   // Reference to a non-existing internal zettel
	RefStateHosted                   // Reference to local hosted non-Zettel, without URL change
	RefStateBased                    // Reference to local non-Zettel, to be prefixed
	RefStateExternal                 // Reference to external material
)

Changes to ast/attr.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
	for k, v := range a.Attrs {
		attrs[k] = v
	}
	return &Attributes{attrs}
}

// Set changes the attribute that a given key has now a given value.
func (a *Attributes) Set(key string, value string) *Attributes {
	if a == nil {
		return &Attributes{map[string]string{key: value}}
	}
	if a.Attrs == nil {
		a.Attrs = make(map[string]string)
	}
	a.Attrs[key] = value







|







52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
	for k, v := range a.Attrs {
		attrs[k] = v
	}
	return &Attributes{attrs}
}

// Set changes the attribute that a given key has now a given value.
func (a *Attributes) Set(key, value string) *Attributes {
	if a == nil {
		return &Attributes{map[string]string{key: value}}
	}
	if a.Attrs == nil {
		a.Attrs = make(map[string]string)
	}
	a.Attrs[key] = value

Changes to ast/ref.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23









24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

45


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77


78
79
80
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 ast provides the abstract syntax tree.
package ast

import (
	"net/url"

	"zettelstore.de/z/domain/id"
)

// ParseReference parses a string and returns a reference.
func ParseReference(s string) *Reference {
	if len(s) == 0 {
		return &Reference{URL: nil, Value: s, State: RefStateInvalid}









	}
	u, err := url.Parse(s)
	if err != nil {
		return &Reference{URL: nil, Value: s, State: RefStateInvalid}
	}
	if len(u.Scheme)+len(u.Opaque)+len(u.Host) == 0 && u.User == nil {
		if _, err := id.Parse(u.Path); err == nil {
			return &Reference{URL: u, Value: s, State: RefStateZettel}
		}
		if u.Path == "" && u.Fragment != "" {
			return &Reference{URL: u, Value: s, State: RefStateZettelSelf}
		}
		if isLocalPath(u.Path) {
			return &Reference{URL: u, Value: s, State: RefStateLocal}
		}
	}
	return &Reference{URL: u, Value: s, State: RefStateExternal}
}

func isLocalPath(path string) bool {
	if len(path) > 0 && path[0] == '/' {

		return true


	}
	if len(path) > 1 && path[0] == '.' {
		if len(path) > 2 && path[1] == '.' && path[2] == '/' {
			return true
		}
		return path[1] == '/'
	}
	return false
}

// String returns the string representation of a reference.
func (r Reference) String() string {
	if r.URL != nil {
		return r.URL.String()
	}
	return r.Value
}

// IsValid returns true if reference is valid
func (r *Reference) IsValid() bool { return r.State != RefStateInvalid }

// IsZettel returns true if it is a referencen to a local zettel.
func (r *Reference) IsZettel() bool {
	switch r.State {
	case RefStateZettel, RefStateZettelSelf, RefStateZettelFound, RefStateZettelBroken:
		return true
	}
	return false
}

// IsLocal returns true if reference is local
func (r *Reference) IsLocal() bool { return r.State == RefStateLocal }



// IsExternal returns true if it is a referencen to external material.
func (r *Reference) IsExternal() bool { return r.State == RefStateExternal }

|



















|

>
>
>
>
>
>
>
>
>










|
<
<
<





|

>
|
>
>



|

|

|
















|






|
>
>



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
//-----------------------------------------------------------------------------
// 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 ast provides the abstract syntax tree.
package ast

import (
	"net/url"

	"zettelstore.de/z/domain/id"
)

// ParseReference parses a string and returns a reference.
func ParseReference(s string) *Reference {
	if s == "" {
		return &Reference{URL: nil, Value: s, State: RefStateInvalid}
	}
	if state, ok := localState(s); ok {
		if state == RefStateBased {
			s = s[1:]
		}
		u, err := url.Parse(s)
		if err == nil {
			return &Reference{URL: u, Value: s, State: state}
		}
	}
	u, err := url.Parse(s)
	if err != nil {
		return &Reference{URL: nil, Value: s, State: RefStateInvalid}
	}
	if len(u.Scheme)+len(u.Opaque)+len(u.Host) == 0 && u.User == nil {
		if _, err := id.Parse(u.Path); err == nil {
			return &Reference{URL: u, Value: s, State: RefStateZettel}
		}
		if u.Path == "" && u.Fragment != "" {
			return &Reference{URL: u, Value: s, State: RefStateSelf}



		}
	}
	return &Reference{URL: u, Value: s, State: RefStateExternal}
}

func localState(path string) (RefState, bool) {
	if len(path) > 0 && path[0] == '/' {
		if len(path) > 1 && path[1] == '/' {
			return RefStateBased, true
		}
		return RefStateHosted, true
	}
	if len(path) > 1 && path[0] == '.' {
		if len(path) > 2 && path[1] == '.' && path[2] == '/' {
			return RefStateHosted, true
		}
		return RefStateHosted, path[1] == '/'
	}
	return RefStateInvalid, false
}

// String returns the string representation of a reference.
func (r Reference) String() string {
	if r.URL != nil {
		return r.URL.String()
	}
	return r.Value
}

// IsValid returns true if reference is valid
func (r *Reference) IsValid() bool { return r.State != RefStateInvalid }

// IsZettel returns true if it is a referencen to a local zettel.
func (r *Reference) IsZettel() bool {
	switch r.State {
	case RefStateZettel, RefStateSelf, RefStateFound, RefStateBroken:
		return true
	}
	return false
}

// IsLocal returns true if reference is local
func (r *Reference) IsLocal() bool {
	return r.State == RefStateHosted || r.State == RefStateBased
}

// IsExternal returns true if it is a referencen to external material.
func (r *Reference) IsExternal() bool { return r.State == RefStateExternal }

Changes to ast/ref_test.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
51
52
53
54
55
56
57

58
59
60
61
62
63
64
		{"http://zettelstore.de/z/ast", false, true, false},
		{"12345678901234", true, false, false},
		{"12345678901234#local", true, false, false},
		{"http://12345678901234", false, true, false},
		{"http://zettelstore.de/z/12345678901234", false, true, false},
		{"http://zettelstore.de/12345678901234", false, true, false},
		{"/12345678901234", false, false, true},

		{"./12345678901234", false, false, true},
		{"../12345678901234", false, false, true},
		{".../12345678901234", false, true, false},
	}

	for i, tc := range testcases {
		ref := ast.ParseReference(tc.link)







>







51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
		{"http://zettelstore.de/z/ast", false, true, false},
		{"12345678901234", true, false, false},
		{"12345678901234#local", true, false, false},
		{"http://12345678901234", false, true, false},
		{"http://zettelstore.de/z/12345678901234", false, true, false},
		{"http://zettelstore.de/12345678901234", false, true, false},
		{"/12345678901234", false, false, true},
		{"//12345678901234", false, false, true},
		{"./12345678901234", false, false, true},
		{"../12345678901234", false, false, true},
		{".../12345678901234", false, true, false},
	}

	for i, tc := range testcases {
		ref := ast.ParseReference(tc.link)

Changes to auth/cred/cred.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 cred provides some function for handling credentials.
package cred

import (
	"bytes"

	"golang.org/x/crypto/bcrypt"
	"zettelstore.de/z/domain/id"
)

// HashCredential returns a hashed vesion of the given credential
func HashCredential(zid id.Zid, ident string, credential string) (string, error) {
	fullCredential := createFullCredential(zid, ident, credential)
	res, err := bcrypt.GenerateFromPassword(fullCredential, bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}
	return string(res), nil
}

// CompareHashAndCredential checks, whether the hashed credential is a possible
// value when hashing the credential.
func CompareHashAndCredential(hashed string, zid id.Zid, ident string, credential string) (bool, error) {
	fullCredential := createFullCredential(zid, ident, credential)
	err := bcrypt.CompareHashAndPassword([]byte(hashed), fullCredential)
	if err == nil {
		return true, nil
	}
	if err == bcrypt.ErrMismatchedHashAndPassword {
		return false, nil
	}
	return false, err
}

func createFullCredential(zid id.Zid, ident string, credential string) []byte {
	var buf bytes.Buffer
	buf.WriteString(zid.String())
	buf.WriteByte(' ')
	buf.WriteString(ident)
	buf.WriteByte(' ')
	buf.WriteString(credential)
	return buf.Bytes()
}

|



















|










|











|








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//-----------------------------------------------------------------------------
// Copyright (c) 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 cred provides some function for handling credentials.
package cred

import (
	"bytes"

	"golang.org/x/crypto/bcrypt"
	"zettelstore.de/z/domain/id"
)

// HashCredential returns a hashed vesion of the given credential
func HashCredential(zid id.Zid, ident, credential string) (string, error) {
	fullCredential := createFullCredential(zid, ident, credential)
	res, err := bcrypt.GenerateFromPassword(fullCredential, bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}
	return string(res), nil
}

// CompareHashAndCredential checks, whether the hashed credential is a possible
// value when hashing the credential.
func CompareHashAndCredential(hashed string, zid id.Zid, ident, credential string) (bool, error) {
	fullCredential := createFullCredential(zid, ident, credential)
	err := bcrypt.CompareHashAndPassword([]byte(hashed), fullCredential)
	if err == nil {
		return true, nil
	}
	if err == bcrypt.ErrMismatchedHashAndPassword {
		return false, nil
	}
	return false, err
}

func createFullCredential(zid id.Zid, ident, credential string) []byte {
	var buf bytes.Buffer
	buf.WriteString(zid.String())
	buf.WriteByte(' ')
	buf.WriteString(ident)
	buf.WriteByte(' ')
	buf.WriteString(credential)
	return buf.Bytes()
}

Changes to auth/policy/anon.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
type anonPolicy struct {
	simpleMode    bool
	expertMode    func() bool
	getVisibility func(*meta.Meta) meta.Visibility
	pre           Policy
}

func (ap *anonPolicy) CanReload(user *meta.Meta) bool {
	return ap.pre.CanReload(user)
}

func (ap *anonPolicy) CanCreate(user *meta.Meta, newMeta *meta.Meta) bool {
	return ap.pre.CanCreate(user, newMeta)
}

func (ap *anonPolicy) CanRead(user *meta.Meta, m *meta.Meta) bool {
	return ap.pre.CanRead(user, m) && ap.checkVisibility(m)
}

func (ap *anonPolicy) CanWrite(user *meta.Meta, oldMeta, newMeta *meta.Meta) bool {
	return ap.pre.CanWrite(user, oldMeta, newMeta) && ap.checkVisibility(oldMeta)
}

func (ap *anonPolicy) CanRename(user *meta.Meta, m *meta.Meta) bool {
	return ap.pre.CanRename(user, m) && ap.checkVisibility(m)
}

func (ap *anonPolicy) CanDelete(user *meta.Meta, m *meta.Meta) bool {
	return ap.pre.CanDelete(user, m) && ap.checkVisibility(m)
}

func (ap *anonPolicy) checkVisibility(m *meta.Meta) bool {
	switch ap.getVisibility(m) {
	case meta.VisibilitySimple:
		return ap.simpleMode || ap.expertMode()
	case meta.VisibilityExpert:
		return ap.expertMode()
	}
	return true
}







<
<
<
<
|



|



|



|



|












18
19
20
21
22
23
24




25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
type anonPolicy struct {
	simpleMode    bool
	expertMode    func() bool
	getVisibility func(*meta.Meta) meta.Visibility
	pre           Policy
}





func (ap *anonPolicy) CanCreate(user, newMeta *meta.Meta) bool {
	return ap.pre.CanCreate(user, newMeta)
}

func (ap *anonPolicy) CanRead(user, m *meta.Meta) bool {
	return ap.pre.CanRead(user, m) && ap.checkVisibility(m)
}

func (ap *anonPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool {
	return ap.pre.CanWrite(user, oldMeta, newMeta) && ap.checkVisibility(oldMeta)
}

func (ap *anonPolicy) CanRename(user, m *meta.Meta) bool {
	return ap.pre.CanRename(user, m) && ap.checkVisibility(m)
}

func (ap *anonPolicy) CanDelete(user, m *meta.Meta) bool {
	return ap.pre.CanDelete(user, m) && ap.checkVisibility(m)
}

func (ap *anonPolicy) checkVisibility(m *meta.Meta) bool {
	switch ap.getVisibility(m) {
	case meta.VisibilitySimple:
		return ap.simpleMode || ap.expertMode()
	case meta.VisibilityExpert:
		return ap.expertMode()
	}
	return true
}

Changes to auth/policy/default.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 policy provides some interfaces and implementation for authorizsation policies.
package policy

import (
	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/domain/meta"
)

type defaultPolicy struct{}

func (d *defaultPolicy) CanReload(user *meta.Meta) bool {
	return true
}

func (d *defaultPolicy) CanCreate(user *meta.Meta, newMeta *meta.Meta) bool {
	return true
}

func (d *defaultPolicy) CanRead(user *meta.Meta, m *meta.Meta) bool {
	return true
}

func (d *defaultPolicy) CanWrite(user *meta.Meta, oldMeta, newMeta *meta.Meta) bool {
	return d.canChange(user, oldMeta)
}

func (d *defaultPolicy) CanRename(user *meta.Meta, m *meta.Meta) bool {
	return d.canChange(user, m)
}

func (d *defaultPolicy) CanDelete(user *meta.Meta, m *meta.Meta) bool {
	return d.canChange(user, m)
}

func (d *defaultPolicy) canChange(user *meta.Meta, m *meta.Meta) bool {
	metaRo, ok := m.Get(meta.KeyReadOnly)
	if !ok {
		return true
	}
	if user == nil {
		// If we are here, there is no authentication.
		// See owner.go:CanWrite.

|


















<
<
<
<
|
<
<
<
|
<
<
<
|


<
|
<
<
<
|
<
|
<
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20




21



22



23
24
25

26



27

28

29
30
31
32
33
34
35
36
//-----------------------------------------------------------------------------
// 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 policy provides some interfaces and implementation for authorizsation policies.
package policy

import (
	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/domain/meta"
)

type defaultPolicy struct{}





func (d *defaultPolicy) CanCreate(user, newMeta *meta.Meta) bool { return true }



func (d *defaultPolicy) CanRead(user, m *meta.Meta) bool         { return true }



func (d *defaultPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool {
	return d.canChange(user, oldMeta)
}

func (d *defaultPolicy) CanRename(user, m *meta.Meta) bool { return d.canChange(user, m) }



func (d *defaultPolicy) CanDelete(user, m *meta.Meta) bool { return d.canChange(user, m) }



func (d *defaultPolicy) canChange(user, m *meta.Meta) bool {
	metaRo, ok := m.Get(meta.KeyReadOnly)
	if !ok {
		return true
	}
	if user == nil {
		// If we are here, there is no authentication.
		// See owner.go:CanWrite.

Changes to auth/policy/owner.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
type ownerPolicy struct {
	expertMode    func() bool
	isOwner       func(id.Zid) bool
	getVisibility func(*meta.Meta) meta.Visibility
	pre           Policy
}

func (o *ownerPolicy) CanReload(user *meta.Meta) bool {
	// No need to call o.pre.CanReload(user), because it will always return true.
	// Both the default and the readonly policy allow to reload a place.

	// Only the owner is allowed to reload a place
	return o.userIsOwner(user)
}

func (o *ownerPolicy) CanCreate(user *meta.Meta, newMeta *meta.Meta) bool {
	if user == nil || !o.pre.CanCreate(user, newMeta) {
		return false
	}
	return o.userIsOwner(user) || o.userCanCreate(user, newMeta)
}

func (o *ownerPolicy) userCanCreate(user *meta.Meta, newMeta *meta.Meta) bool {
	if runtime.GetUserRole(user) == meta.UserRoleReader {
		return false
	}
	if role, ok := newMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser {
		return false
	}
	return true
}

func (o *ownerPolicy) CanRead(user *meta.Meta, m *meta.Meta) bool {
	// No need to call o.pre.CanRead(user, meta), because it will always return true.
	// Both the default and the readonly policy allow to read a zettel.
	vis := o.getVisibility(m)
	if res, ok := o.checkVisibility(user, vis); ok {
		return res
	}
	return o.userIsOwner(user) || o.userCanRead(user, m, vis)
}

func (o *ownerPolicy) userCanRead(user *meta.Meta, m *meta.Meta, vis meta.Visibility) bool {
	switch vis {
	case meta.VisibilityOwner, meta.VisibilitySimple, meta.VisibilityExpert:
		return false
	case meta.VisibilityPublic:
		return true
	}
	if user == nil {







<
<
<
<
<
<
<
<
|






|









|









|







20
21
22
23
24
25
26








27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
type ownerPolicy struct {
	expertMode    func() bool
	isOwner       func(id.Zid) bool
	getVisibility func(*meta.Meta) meta.Visibility
	pre           Policy
}









func (o *ownerPolicy) CanCreate(user, newMeta *meta.Meta) bool {
	if user == nil || !o.pre.CanCreate(user, newMeta) {
		return false
	}
	return o.userIsOwner(user) || o.userCanCreate(user, newMeta)
}

func (o *ownerPolicy) userCanCreate(user, newMeta *meta.Meta) bool {
	if runtime.GetUserRole(user) == meta.UserRoleReader {
		return false
	}
	if role, ok := newMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser {
		return false
	}
	return true
}

func (o *ownerPolicy) CanRead(user, m *meta.Meta) bool {
	// No need to call o.pre.CanRead(user, meta), because it will always return true.
	// Both the default and the readonly policy allow to read a zettel.
	vis := o.getVisibility(m)
	if res, ok := o.checkVisibility(user, vis); ok {
		return res
	}
	return o.userIsOwner(user) || o.userCanRead(user, m, vis)
}

func (o *ownerPolicy) userCanRead(user, m *meta.Meta, vis meta.Visibility) bool {
	switch vis {
	case meta.VisibilityOwner, meta.VisibilitySimple, meta.VisibilityExpert:
		return false
	case meta.VisibilityPublic:
		return true
	}
	if user == nil {
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
var noChangeUser = []string{
	meta.KeyID,
	meta.KeyRole,
	meta.KeyUserID,
	meta.KeyUserRole,
}

func (o *ownerPolicy) CanWrite(user *meta.Meta, oldMeta, newMeta *meta.Meta) bool {
	if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) {
		return false
	}
	vis := o.getVisibility(oldMeta)
	if res, ok := o.checkVisibility(user, vis); ok {
		return res
	}







|







71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
var noChangeUser = []string{
	meta.KeyID,
	meta.KeyRole,
	meta.KeyUserID,
	meta.KeyUserRole,
}

func (o *ownerPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool {
	if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) {
		return false
	}
	vis := o.getVisibility(oldMeta)
	if res, ok := o.checkVisibility(user, vis); ok {
		return res
	}
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
	}
	if runtime.GetUserRole(user) == meta.UserRoleReader {
		return false
	}
	return o.userCanCreate(user, newMeta)
}

func (o *ownerPolicy) CanRename(user *meta.Meta, m *meta.Meta) bool {
	if user == nil || !o.pre.CanRename(user, m) {
		return false
	}
	if res, ok := o.checkVisibility(user, o.getVisibility(m)); ok {
		return res
	}
	return o.userIsOwner(user)
}

func (o *ownerPolicy) CanDelete(user *meta.Meta, m *meta.Meta) bool {
	if user == nil || !o.pre.CanDelete(user, m) {
		return false
	}
	if res, ok := o.checkVisibility(user, o.getVisibility(m)); ok {
		return res
	}
	return o.userIsOwner(user)







|









|







101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
	}
	if runtime.GetUserRole(user) == meta.UserRoleReader {
		return false
	}
	return o.userCanCreate(user, newMeta)
}

func (o *ownerPolicy) CanRename(user, m *meta.Meta) bool {
	if user == nil || !o.pre.CanRename(user, m) {
		return false
	}
	if res, ok := o.checkVisibility(user, o.getVisibility(m)); ok {
		return res
	}
	return o.userIsOwner(user)
}

func (o *ownerPolicy) CanDelete(user, m *meta.Meta) bool {
	if user == nil || !o.pre.CanDelete(user, m) {
		return false
	}
	if res, ok := o.checkVisibility(user, o.getVisibility(m)); ok {
		return res
	}
	return o.userIsOwner(user)

Changes to auth/policy/place.go.

53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
	return pp.place.Location()
}

func (pp *polPlace) CanCreateZettel(ctx context.Context) bool {
	return pp.place.CanCreateZettel(ctx)
}

func (pp *polPlace) CreateZettel(
	ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	user := session.GetUser(ctx)
	if pp.policy.CanCreate(user, zettel.Meta) {
		return pp.place.CreateZettel(ctx, zettel)
	}
	return id.Invalid, place.NewErrNotAllowed("Create", user, id.Invalid)
}








<
|







53
54
55
56
57
58
59

60
61
62
63
64
65
66
67
	return pp.place.Location()
}

func (pp *polPlace) CanCreateZettel(ctx context.Context) bool {
	return pp.place.CanCreateZettel(ctx)
}


func (pp *polPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	user := session.GetUser(ctx)
	if pp.policy.CanCreate(user, zettel.Meta) {
		return pp.place.CreateZettel(ctx, zettel)
	}
	return id.Invalid, place.NewErrNotAllowed("Create", user, id.Invalid)
}

86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
	user := session.GetUser(ctx)
	if pp.policy.CanRead(user, m) {
		return m, nil
	}
	return nil, place.NewErrNotAllowed("GetMeta", user, zid)
}

func (pp *polPlace) FetchZids(ctx context.Context) (map[id.Zid]bool, error) {
	return nil, place.NewErrNotAllowed("fetch-zids", session.GetUser(ctx), id.Invalid)
}

func (pp *polPlace) SelectMeta(
	ctx context.Context, f *place.Filter, s *place.Sorter) ([]*meta.Meta, error) {
	user := session.GetUser(ctx)
	f = place.EnsureFilter(f)







|







85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
	user := session.GetUser(ctx)
	if pp.policy.CanRead(user, m) {
		return m, nil
	}
	return nil, place.NewErrNotAllowed("GetMeta", user, zid)
}

func (pp *polPlace) FetchZids(ctx context.Context) (id.Set, error) {
	return nil, place.NewErrNotAllowed("fetch-zids", session.GetUser(ctx), id.Invalid)
}

func (pp *polPlace) SelectMeta(
	ctx context.Context, f *place.Filter, s *place.Sorter) ([]*meta.Meta, error) {
	user := session.GetUser(ctx)
	f = place.EnsureFilter(f)
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
	user := session.GetUser(ctx)
	if pp.policy.CanDelete(user, meta) {
		return pp.place.DeleteZettel(ctx, zid)
	}
	return place.NewErrNotAllowed("Delete", user, zid)
}

func (pp *polPlace) Reload(ctx context.Context) error {
	user := session.GetUser(ctx)
	if pp.policy.CanReload(user) {
		return pp.place.Reload(ctx)
	}
	return place.NewErrNotAllowed("Reload", user, id.Invalid)
}
func (pp *polPlace) ReadStats(st *place.Stats) {
	pp.place.ReadStats(st)
}







<
<
<
<
<
<
<



160
161
162
163
164
165
166







167
168
169
	user := session.GetUser(ctx)
	if pp.policy.CanDelete(user, meta) {
		return pp.place.DeleteZettel(ctx, zid)
	}
	return place.NewErrNotAllowed("Delete", user, zid)
}








func (pp *polPlace) ReadStats(st *place.Stats) {
	pp.place.ReadStats(st)
}

Changes to auth/policy/policy.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 policy provides some interfaces and implementation for authorizsation policies.
package policy

import (
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

// Policy is an interface for checking access authorization.
type Policy interface {
	// User is allowed to reload a place.
	CanReload(user *meta.Meta) bool

	// User is allowed to create a new zettel.
	CanCreate(user *meta.Meta, newMeta *meta.Meta) bool

	// User is allowed to read zettel
	CanRead(user *meta.Meta, m *meta.Meta) bool

	// User is allowed to write zettel.
	CanWrite(user *meta.Meta, oldMeta, newMeta *meta.Meta) bool

	// User is allowed to rename zettel
	CanRename(user *meta.Meta, m *meta.Meta) bool

	// User is allowed to delete zettel
	CanDelete(user *meta.Meta, m *meta.Meta) bool
}

// newPolicy creates a policy based on given constraints.
func newPolicy(
	simpleMode bool,
	withAuth func() bool,
	isReadOnlyMode bool,

|


















<
<
<

|


|


|


|


|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//-----------------------------------------------------------------------------
// Copyright (c) 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 policy provides some interfaces and implementation for authorizsation policies.
package policy

import (
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

// Policy is an interface for checking access authorization.
type Policy interface {



	// User is allowed to create a new zettel.
	CanCreate(user, newMeta *meta.Meta) bool

	// User is allowed to read zettel
	CanRead(user, m *meta.Meta) bool

	// User is allowed to write zettel.
	CanWrite(user, oldMeta, newMeta *meta.Meta) bool

	// User is allowed to rename zettel
	CanRename(user, m *meta.Meta) bool

	// User is allowed to delete zettel
	CanDelete(user, m *meta.Meta) bool
}

// newPolicy creates a policy based on given constraints.
func newPolicy(
	simpleMode bool,
	withAuth func() bool,
	isReadOnlyMode bool,
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
	return &prePolicy{pol}
}

type prePolicy struct {
	post Policy
}

func (p *prePolicy) CanReload(user *meta.Meta) bool {
	return p.post.CanReload(user)
}

func (p *prePolicy) CanCreate(user *meta.Meta, newMeta *meta.Meta) bool {
	return newMeta != nil && p.post.CanCreate(user, newMeta)
}

func (p *prePolicy) CanRead(user *meta.Meta, m *meta.Meta) bool {
	return m != nil && p.post.CanRead(user, m)
}

func (p *prePolicy) CanWrite(user *meta.Meta, oldMeta, newMeta *meta.Meta) bool {
	return oldMeta != nil && newMeta != nil && oldMeta.Zid == newMeta.Zid &&
		p.post.CanWrite(user, oldMeta, newMeta)
}

func (p *prePolicy) CanRename(user *meta.Meta, m *meta.Meta) bool {
	return m != nil && p.post.CanRename(user, m)
}

func (p *prePolicy) CanDelete(user *meta.Meta, m *meta.Meta) bool {
	return m != nil && p.post.CanDelete(user, m)
}







<
<
<
<
|



|



|




|



|


67
68
69
70
71
72
73




74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
	return &prePolicy{pol}
}

type prePolicy struct {
	post Policy
}





func (p *prePolicy) CanCreate(user, newMeta *meta.Meta) bool {
	return newMeta != nil && p.post.CanCreate(user, newMeta)
}

func (p *prePolicy) CanRead(user, m *meta.Meta) bool {
	return m != nil && p.post.CanRead(user, m)
}

func (p *prePolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool {
	return oldMeta != nil && newMeta != nil && oldMeta.Zid == newMeta.Zid &&
		p.post.CanWrite(user, oldMeta, newMeta)
}

func (p *prePolicy) CanRename(user, m *meta.Meta) bool {
	return m != nil && p.post.CanRename(user, m)
}

func (p *prePolicy) CanDelete(user, m *meta.Meta) bool {
	return m != nil && p.post.CanDelete(user, m)
}

Changes to auth/policy/policy_test.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
		} else {
			expertFunc = noExpertMode
		}
		pol := newPolicy(ts.simple, authFunc, ts.readonly, expertFunc, isOwner, getVisibility)
		name := fmt.Sprintf("simple=%v/readonly=%v/withauth=%v/expert=%v",
			ts.simple, ts.readonly, ts.withAuth, ts.expert)
		t.Run(name, func(tt *testing.T) {
			testReload(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert)
			testCreate(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert)
			testRead(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert)
			testWrite(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert)
			testRename(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert)
			testDelete(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert)
		})
	}







<







56
57
58
59
60
61
62

63
64
65
66
67
68
69
		} else {
			expertFunc = noExpertMode
		}
		pol := newPolicy(ts.simple, authFunc, ts.readonly, expertFunc, isOwner, getVisibility)
		name := fmt.Sprintf("simple=%v/readonly=%v/withauth=%v/expert=%v",
			ts.simple, ts.readonly, ts.withAuth, ts.expert)
		t.Run(name, func(tt *testing.T) {

			testCreate(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert)
			testRead(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert)
			testWrite(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert)
			testRename(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert)
			testDelete(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert)
		})
	}
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
		case meta.ValueVisibilitySimple:
			return meta.VisibilitySimple
		}
	}
	return meta.VisibilityLogin
}

func testReload(t *testing.T, pol Policy, simple bool, withAuth bool, readonly bool, isExpert bool) {
	t.Helper()
	testCases := []struct {
		user *meta.Meta
		exp  bool
	}{
		{newAnon(), !withAuth},
		{newReader(), !withAuth},
		{newWriter(), !withAuth},
		{newOwner(), true},
	}
	for _, tc := range testCases {
		t.Run("Reload", func(tt *testing.T) {
			got := pol.CanReload(tc.user)
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testCreate(t *testing.T, pol Policy, simple bool, withAuth bool, readonly bool, isExpert bool) {
	t.Helper()
	anonUser := newAnon()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|







86
87
88
89
90
91
92





















93
94
95
96
97
98
99
100
		case meta.ValueVisibilitySimple:
			return meta.VisibilitySimple
		}
	}
	return meta.VisibilityLogin
}






















func testCreate(t *testing.T, pol Policy, simple, withAuth, readonly, isExpert bool) {
	t.Helper()
	anonUser := newAnon()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testRead(t *testing.T, pol Policy, simple bool, withAuth bool, readonly bool, expert bool) {
	t.Helper()
	anonUser := newAnon()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()







|







129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testRead(t *testing.T, pol Policy, simple, withAuth, readonly, expert bool) {
	t.Helper()
	anonUser := newAnon()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testWrite(t *testing.T, pol Policy, simple bool, withAuth bool, readonly bool, expert bool) {
	t.Helper()
	anonUser := newAnon()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()







|







214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testWrite(t *testing.T, pol Policy, simple, withAuth, readonly, expert bool) {
	t.Helper()
	anonUser := newAnon()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testRename(t *testing.T, pol Policy, simple bool, withAuth bool, readonly bool, expert bool) {
	t.Helper()
	anonUser := newAnon()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()







|







355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testRename(t *testing.T, pol Policy, simple, withAuth, readonly, expert bool) {
	t.Helper()
	anonUser := newAnon()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testDelete(t *testing.T, pol Policy, simple bool, withAuth bool, readonly bool, expert bool) {
	t.Helper()
	anonUser := newAnon()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()







|







440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testDelete(t *testing.T, pol Policy, simple, withAuth, readonly, expert bool) {
	t.Helper()
	anonUser := newAnon()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()

Changes to auth/policy/readonly.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 policy provides some interfaces and implementation for authorization policies.
package policy

import "zettelstore.de/z/domain/meta"

type roPolicy struct{}

func (p *roPolicy) CanReload(user *meta.Meta) bool {
	return true
}

func (p *roPolicy) CanCreate(user *meta.Meta, newMeta *meta.Meta) bool {
	return false
}

func (p *roPolicy) CanRead(user *meta.Meta, m *meta.Meta) bool {
	return true
}

func (p *roPolicy) CanWrite(user *meta.Meta, oldMeta, newMeta *meta.Meta) bool {
	return false
}

func (p *roPolicy) CanRename(user *meta.Meta, m *meta.Meta) bool {
	return false
}

func (p *roPolicy) CanDelete(user *meta.Meta, m *meta.Meta) bool {
	return false
}

|















<
<
<
<
|
<
<
<
|
<
<
<
|
<
<
<
|
<
<
<
|
<
<
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17




18



19



20



21



22


//-----------------------------------------------------------------------------
// 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 policy provides some interfaces and implementation for authorization policies.
package policy

import "zettelstore.de/z/domain/meta"

type roPolicy struct{}





func (p *roPolicy) CanCreate(user, newMeta *meta.Meta) bool         { return false }



func (p *roPolicy) CanRead(user, m *meta.Meta) bool                 { return true }



func (p *roPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return false }



func (p *roPolicy) CanRename(user, m *meta.Meta) bool               { return false }



func (p *roPolicy) CanDelete(user, m *meta.Meta) bool               { return false }


Changes to auth/token/token.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

// GetToken returns a token to be used for authentification.
func GetToken(ident *meta.Meta, d time.Duration, kind Kind) ([]byte, error) {
	if role, ok := ident.Get(meta.KeyRole); !ok || role != meta.ValueRoleUser {
		return nil, ErrNoUser
	}
	subject, ok := ident.Get(meta.KeyUserID)
	if !ok || len(subject) == 0 {
		return nil, ErrNoIdent
	}

	now := time.Now().Round(time.Second)
	claims := jwt.Claims{
		Registered: jwt.Registered{
			Subject: subject,







|







48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

// GetToken returns a token to be used for authentification.
func GetToken(ident *meta.Meta, d time.Duration, kind Kind) ([]byte, error) {
	if role, ok := ident.Get(meta.KeyRole); !ok || role != meta.ValueRoleUser {
		return nil, ErrNoUser
	}
	subject, ok := ident.Get(meta.KeyUserID)
	if !ok || subject == "" {
		return nil, ErrNoIdent
	}

	now := time.Now().Round(time.Second)
	claims := jwt.Claims{
		Registered: jwt.Registered{
			Subject: subject,
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
	}
	now := time.Now().Round(time.Second)
	expires := claims.Expires.Time()
	if expires.Before(now) {
		return Data{}, ErrTokenExpired
	}
	ident := claims.Subject
	if len(ident) == 0 {
		return Data{}, ErrNoIdent
	}
	if zidS, ok := claims.Set["zid"].(string); ok {
		if zid, err := id.Parse(zidS); err == nil {
			if kind, ok := claims.Set["_tk"].(float64); ok {
				if Kind(kind) == k {
					return Data{







|







100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
	}
	now := time.Now().Round(time.Second)
	expires := claims.Expires.Time()
	if expires.Before(now) {
		return Data{}, ErrTokenExpired
	}
	ident := claims.Subject
	if ident == "" {
		return Data{}, ErrNoIdent
	}
	if zidS, ok := claims.Set["zid"].(string); ok {
		if zid, err := id.Parse(zidS); err == nil {
			if kind, ok := claims.Set["_tk"].(float64); ok {
				if Kind(kind) == k {
					return Data{

Changes to cmd/cmd_run.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100




101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116




117


118

119

120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
	ucAuthenticate := usecase.NewAuthenticate(up)
	ucGetMeta := usecase.NewGetMeta(pp)
	ucGetZettel := usecase.NewGetZettel(pp)
	ucParseZettel := usecase.NewParseZettel(ucGetZettel)
	ucListMeta := usecase.NewListMeta(pp)
	ucListRoles := usecase.NewListRole(pp)
	ucListTags := usecase.NewListTags(pp)
	listHTMLMetaHandler := webui.MakeListHTMLMetaHandler(te, ucListMeta)
	getHTMLZettelHandler := webui.MakeGetHTMLZettelHandler(te, ucParseZettel, ucGetMeta)

	router := router.NewRouter()
	router.Handle("/", webui.MakeGetRootHandler(
		pp, listHTMLMetaHandler, getHTMLZettelHandler))
	router.AddListRoute('a', http.MethodGet, webui.MakeGetLoginHandler(te))
	router.AddListRoute('a', http.MethodPost, adapter.MakePostLoginHandler(
		api.MakePostLoginHandlerAPI(ucAuthenticate),
		webui.MakePostLoginHandlerHTML(te, ucAuthenticate)))
	router.AddListRoute('a', http.MethodPut, api.MakeRenewAuthHandler())
	router.AddZettelRoute('a', http.MethodGet, webui.MakeGetLogoutHandler())
	router.AddListRoute('c', http.MethodGet, adapter.MakeReloadHandler(
		usecase.NewReload(pp), api.ReloadHandlerAPI, webui.ReloadHandlerHTML))
	if !readonlyMode {




		router.AddZettelRoute('c', http.MethodGet, webui.MakeGetCopyZettelHandler(
			te, ucGetZettel, usecase.NewCopyZettel()))
		router.AddZettelRoute('c', http.MethodPost, webui.MakePostCreateZettelHandler(
			usecase.NewCreateZettel(pp)))
		router.AddZettelRoute('d', http.MethodGet, webui.MakeGetDeleteZettelHandler(
			te, ucGetZettel))
		router.AddZettelRoute('d', http.MethodPost, webui.MakePostDeleteZettelHandler(
			usecase.NewDeleteZettel(pp)))
		router.AddZettelRoute('e', http.MethodGet, webui.MakeEditGetZettelHandler(
			te, ucGetZettel))
		router.AddZettelRoute('e', http.MethodPost, webui.MakeEditSetZettelHandler(
			usecase.NewUpdateZettel(pp)))
		router.AddZettelRoute('f', http.MethodGet, webui.MakeGetFolgeZettelHandler(
			te, ucGetZettel, usecase.NewFolgeZettel()))
		router.AddZettelRoute('f', http.MethodPost, webui.MakePostCreateZettelHandler(
			usecase.NewCreateZettel(pp)))




	}


	router.AddListRoute('h', http.MethodGet, listHTMLMetaHandler)

	router.AddZettelRoute('h', http.MethodGet, getHTMLZettelHandler)

	router.AddZettelRoute('i', http.MethodGet, webui.MakeGetInfoHandler(
		te, ucParseZettel, ucGetMeta))
	router.AddZettelRoute('k', http.MethodGet, webui.MakeWebUIListsHandler(
		te, ucListMeta, ucListRoles, ucListTags))
	router.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel))
	if !readonlyMode {
		router.AddZettelRoute('n', http.MethodGet, webui.MakeGetNewZettelHandler(
			te, ucGetZettel, usecase.NewNewZettel()))
		router.AddZettelRoute('n', http.MethodPost, webui.MakePostCreateZettelHandler(
			usecase.NewCreateZettel(pp)))
	}
	router.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles))
	if !readonlyMode {
		router.AddZettelRoute('r', http.MethodGet, webui.MakeGetRenameZettelHandler(
			te, ucGetMeta))
		router.AddZettelRoute('r', http.MethodPost, webui.MakePostRenameZettelHandler(
			usecase.NewRenameZettel(pp)))
	}
	router.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags))
	router.AddListRoute('s', http.MethodGet, webui.MakeSearchHandler(
		te, usecase.NewSearch(pp), ucGetMeta, ucGetZettel))
	router.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler(
		usecase.NewListMeta(pp), ucGetMeta, ucParseZettel))
	router.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler(
		ucParseZettel, ucGetMeta))
	return session.NewHandler(router, usecase.NewGetUserByZid(up))
}







|
<


|
<






<
<

>
>
>
>
















>
>
>
>

>
>
|
>
|
>


|
|

<
|
<
<
|
<

<
<
<
<
<
<

|
<






79
80
81
82
83
84
85
86

87
88
89

90
91
92
93
94
95


96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

133


134

135






136
137

138
139
140
141
142
143
	ucAuthenticate := usecase.NewAuthenticate(up)
	ucGetMeta := usecase.NewGetMeta(pp)
	ucGetZettel := usecase.NewGetZettel(pp)
	ucParseZettel := usecase.NewParseZettel(ucGetZettel)
	ucListMeta := usecase.NewListMeta(pp)
	ucListRoles := usecase.NewListRole(pp)
	ucListTags := usecase.NewListTags(pp)
	ucZettelContext := usecase.NewZettelContext(pp)


	router := router.NewRouter()
	router.Handle("/", webui.MakeGetRootHandler(pp))

	router.AddListRoute('a', http.MethodGet, webui.MakeGetLoginHandler(te))
	router.AddListRoute('a', http.MethodPost, adapter.MakePostLoginHandler(
		api.MakePostLoginHandlerAPI(ucAuthenticate),
		webui.MakePostLoginHandlerHTML(te, ucAuthenticate)))
	router.AddListRoute('a', http.MethodPut, api.MakeRenewAuthHandler())
	router.AddZettelRoute('a', http.MethodGet, webui.MakeGetLogoutHandler())


	if !readonlyMode {
		router.AddZettelRoute('b', http.MethodGet, webui.MakeGetRenameZettelHandler(
			te, ucGetMeta))
		router.AddZettelRoute('b', http.MethodPost, webui.MakePostRenameZettelHandler(
			usecase.NewRenameZettel(pp)))
		router.AddZettelRoute('c', http.MethodGet, webui.MakeGetCopyZettelHandler(
			te, ucGetZettel, usecase.NewCopyZettel()))
		router.AddZettelRoute('c', http.MethodPost, webui.MakePostCreateZettelHandler(
			usecase.NewCreateZettel(pp)))
		router.AddZettelRoute('d', http.MethodGet, webui.MakeGetDeleteZettelHandler(
			te, ucGetZettel))
		router.AddZettelRoute('d', http.MethodPost, webui.MakePostDeleteZettelHandler(
			usecase.NewDeleteZettel(pp)))
		router.AddZettelRoute('e', http.MethodGet, webui.MakeEditGetZettelHandler(
			te, ucGetZettel))
		router.AddZettelRoute('e', http.MethodPost, webui.MakeEditSetZettelHandler(
			usecase.NewUpdateZettel(pp)))
		router.AddZettelRoute('f', http.MethodGet, webui.MakeGetFolgeZettelHandler(
			te, ucGetZettel, usecase.NewFolgeZettel()))
		router.AddZettelRoute('f', http.MethodPost, webui.MakePostCreateZettelHandler(
			usecase.NewCreateZettel(pp)))
		router.AddZettelRoute('g', http.MethodGet, webui.MakeGetNewZettelHandler(
			te, ucGetZettel, usecase.NewNewZettel()))
		router.AddZettelRoute('g', http.MethodPost, webui.MakePostCreateZettelHandler(
			usecase.NewCreateZettel(pp)))
	}
	router.AddListRoute('f', http.MethodGet, webui.MakeSearchHandler(
		te, usecase.NewSearch(pp), ucGetMeta, ucGetZettel))
	router.AddListRoute('h', http.MethodGet, webui.MakeListHTMLMetaHandler(
		te, ucListMeta, ucListRoles, ucListTags))
	router.AddZettelRoute('h', http.MethodGet, webui.MakeGetHTMLZettelHandler(
		te, ucParseZettel, ucGetMeta))
	router.AddZettelRoute('i', http.MethodGet, webui.MakeGetInfoHandler(
		te, ucParseZettel, ucGetMeta))
	router.AddZettelRoute('j', http.MethodGet, webui.MakeZettelContextHandler(te, ucZettelContext))

	router.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel))

	router.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler(


		usecase.NewZettelOrder(pp, ucParseZettel)))

	router.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles))






	router.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags))
	router.AddZettelRoute('y', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext))

	router.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler(
		usecase.NewListMeta(pp), ucGetMeta, ucParseZettel))
	router.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler(
		ucParseZettel, ucGetMeta))
	return session.NewHandler(router, usecase.NewGetUserByZid(up))
}

Changes to cmd/cmd_run_simple.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 cmd

import (
	"context"
	"flag"
	"fmt"
	"log"
	"os"
	"strings"

	"zettelstore.de/z/domain"
	"zettelstore.de/z/place"
	"zettelstore.de/z/web/server"

	"zettelstore.de/z/config/startup"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

func flgSimpleRun(fs *flag.FlagSet) {
	fs.String("d", "", "zettel directory")
}

func runSimpleFunc(*flag.FlagSet) (int, error) {
	p := startup.PlaceManager()
	if _, err := p.GetMeta(context.Background(), id.WelcomeZid); err != nil {
		if err == place.ErrNotFound {
			updateWelcomeZettel(p)
		}
	}

	listenAddr := startup.ListenAddress()
	readonlyMode := startup.IsReadOnlyMode()
	logBeforeRun(listenAddr, readonlyMode)
	if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 {
		log.Println()
		log.Println("--------------------------")
		log.Printf("Open your browser and enter the following URL:")

|











<






|
<

<
<
<
<







<
<
<
<
<
<
<







1
2
3
4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20

21




22
23
24
25
26
27
28







29
30
31
32
33
34
35
//-----------------------------------------------------------------------------
// Copyright (c) 2020-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 cmd

import (

	"flag"
	"fmt"
	"log"
	"os"
	"strings"

	"zettelstore.de/z/config/startup"

	"zettelstore.de/z/web/server"




)

func flgSimpleRun(fs *flag.FlagSet) {
	fs.String("d", "", "zettel directory")
}

func runSimpleFunc(*flag.FlagSet) (int, error) {







	listenAddr := startup.ListenAddress()
	readonlyMode := startup.IsReadOnlyMode()
	logBeforeRun(listenAddr, readonlyMode)
	if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 {
		log.Println()
		log.Println("--------------------------")
		log.Printf("Open your browser and enter the following URL:")
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
	return 0, nil
}

// runSimple is called, when the user just starts the software via a double click
// or via a simple call ``./zettelstore`` on the command line.
func runSimple() {
	dir := "./zettel"
	if err := os.MkdirAll(dir, 0755); err != nil {
		fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err)
		os.Exit(1)
	}
	executeCommand("run-simple", "-d", dir)
}

func updateWelcomeZettel(p place.Place) {
	m := meta.New(id.WelcomeZid)
	m.Set(meta.KeyTitle, "Welcome to Zettelstore")
	m.Set(meta.KeyRole, meta.ValueRoleZettel)
	m.Set(meta.KeySyntax, meta.ValueSyntaxZmk)
	zid, err := p.CreateZettel(
		context.Background(),
		domain.Zettel{Meta: m, Content: domain.NewContent(welcomeZettelContent)},
	)
	if err == nil {
		p.RenameZettel(context.Background(), zid, id.WelcomeZid)
	}
}

var welcomeZettelContent = `Thank you for using Zettelstore!

You will find the lastest information about Zettelstore at [[https://zettelstore.de]].
Check that website regulary for [[upgrades|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version.
You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before upgrading.
Sometimes, you have to edit some of your Zettelstore-related zettel before upgrading.
Since Zettelstore is currently in a development state, every upgrade might fix some of your problems.
To check for versions, there is a zettel with the [[current version|00000000000001]] of your Zettelstore.

If you have problems concerning Zettelstore,
do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]].

=== Reporting errors
If you have encountered an error, please include the content of the following zettel in your mail:
* [[Zettelstore Version|00000000000001]]
* [[Zettelstore Operating System|00000000000003]]
* [[Zettelstore Startup Configuration|00000000000096]]
* [[Zettelstore Startup Values|00000000000098]]
* [[Zettelstore Runtime Configuration|00000000000100]]

Additionally, you have to describe, what you have done before that error occurs
and what you have expected instead.
Please do not forget to include the error message, if there is one.

Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"".
Otherwise, only some zettel are linked.
To enable expert mode, edit the zettel [[Zettelstore Runtime Configuration|00000000000100]]:
please set the metadata value of the key ''expert-mode'' to true.
To do you, enter the string ''expert-mode:true'' inside the edit view of the metadata.

=== Information about this zettel
This zettel was generated automatically.
Every time you start Zettelstore by double clicking in your graphical user interface,
or by just starting it in a command line via something like ''zettelstore'', and this zettel
does not exist, it will be generated.
This allows you to edit this zettel for your own needs.

If you don't need it anymore, you can delete this zettel by clicking on ""Info"" and then
on ""Delete"".
However, by starting Zettelstore as described above, the original version of this zettel
will be restored.

If you start Zettelstore with the ''run'' command, e.g. as a service or via command line,
this zettel will not be generated.
But if it exists before, it will not be deleted.
In this case, Zettelstore assumes that you have enough knowledge and that you do not need
zettel.
`







|





<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
45
46
47
48
49
50
51
52
53
54
55
56
57































































	return 0, nil
}

// runSimple is called, when the user just starts the software via a double click
// or via a simple call ``./zettelstore`` on the command line.
func runSimple() {
	dir := "./zettel"
	if err := os.MkdirAll(dir, 0750); err != nil {
		fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err)
		os.Exit(1)
	}
	executeCommand("run-simple", "-d", dir)
}































































Changes to cmd/main.go.

120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
		case "v":
			cfg.Set(startup.KeyVerbose, flg.Value.String())
		}
	})
	return cfg
}

func setupOperations(cfg *meta.Meta, withPlaces bool, simple bool) error {
	var mgr place.Manager
	var idx index.Indexer
	if withPlaces {
		idx = indexer.New()
		filter := index.NewMetaFilter(idx)
		p, err := manager.New(getPlaces(cfg), cfg.GetBool(startup.KeyReadOnlyMode), filter)
		if err != nil {







|







120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
		case "v":
			cfg.Set(startup.KeyVerbose, flg.Value.String())
		}
	})
	return cfg
}

func setupOperations(cfg *meta.Meta, withPlaces, simple bool) error {
	var mgr place.Manager
	var idx index.Indexer
	if withPlaces {
		idx = indexer.New()
		filter := index.NewMetaFilter(idx)
		p, err := manager.New(getPlaces(cfg), cfg.GetBool(startup.KeyReadOnlyMode), filter)
		if err != nil {

Changes to cmd/zettelstore/main.go.

12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
	"zettelstore.de/z/cmd"
)

// Version variable. Will be filled by build process.
var buildVersion string = ""

func main() {
	cmd.Main("Zettelstore", buildVersion)
}







|


|

12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
	"zettelstore.de/z/cmd"
)

// Version variable. Will be filled by build process.
var version string = ""

func main() {
	cmd.Main("Zettelstore", version)
}

Changes to collect/collect.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------

Changes to collect/collect_test.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------

Added collect/order.go.











































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//-----------------------------------------------------------------------------
// 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 collect provides functions to collect items from a syntax tree.
package collect

import "zettelstore.de/z/ast"

// Order of internal reference within the given zettel.
func Order(zn *ast.ZettelNode) (result []*ast.Reference) {
	for _, bn := range zn.Ast {
		if ln, ok := bn.(*ast.NestedListNode); ok {
			switch ln.Code {
			case ast.NestedListOrdered, ast.NestedListUnordered:
				for _, is := range ln.Items {
					if ref := firstItemZettelReference(is); ref != nil {
						result = append(result, ref)
					}
				}
			}
		}
	}
	return result
}

func firstItemZettelReference(is ast.ItemSlice) *ast.Reference {
	for _, in := range is {
		if pn, ok := in.(*ast.ParaNode); ok {
			if ref := firstInlineZettelReference(pn.Inlines); ref != nil {
				return ref
			}
		}
	}
	return nil
}

func firstInlineZettelReference(ins ast.InlineSlice) (result *ast.Reference) {
	for _, inl := range ins {
		switch in := inl.(type) {
		case *ast.LinkNode:
			if ref := in.Ref; ref.IsZettel() {
				return ref
			}
			result = firstInlineZettelReference(in.Inlines)
		case *ast.ImageNode:
			result = firstInlineZettelReference(in.Inlines)
		case *ast.CiteNode:
			result = firstInlineZettelReference(in.Inlines)
		case *ast.FootnoteNode:
			// Ignore references in footnotes
			continue
		case *ast.FormatNode:
			result = firstInlineZettelReference(in.Inlines)
		default:
			continue
		}
		if result != nil {
			return result
		}
	}
	return nil
}

Changes to collect/split.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49





50
51
52
53

54
55
56
57
58
59
60
61
62
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 collect provides functions to collect items from a syntax tree.
package collect

import (
	"zettelstore.de/z/ast"
)

// DivideReferences divides the given list of rederences into zettel, local, and external References.
func DivideReferences(all []*ast.Reference, duplicates bool) (zettel, local, external []*ast.Reference) {
	if len(all) == 0 {
		return nil, nil, nil
	}

	mapZettel := make(map[string]bool)
	mapLocal := make(map[string]bool)
	mapExternal := make(map[string]bool)
	for _, ref := range all {
		if ref.State == ast.RefStateZettelSelf {
			continue
		}
		s := ref.String()
		if ref.IsZettel() {
			if duplicates {

				zettel = append(zettel, ref)
			} else {
				if _, ok := mapZettel[s]; !ok {
					zettel = append(zettel, ref)
					mapZettel[s] = true
				}
			}
		} else if ref.IsExternal() {
			if duplicates {
				external = append(external, ref)
			} else {
				if _, ok := mapExternal[s]; !ok {
					external = append(external, ref)
					mapExternal[s] = true
				}
			}





		} else {
			if duplicates {
				local = append(local, ref)
			} else {

				if _, ok := mapLocal[s]; !ok {
					local = append(local, ref)
					mapLocal[s] = true
				}
			}
		}
	}
	return zettel, local, external
}

|











<
|
<











|


<

|
>
|
|
<
|
<
|
|
<
<
<
<
<
<
|
|
|
>
>
>
>
>
|
|
|
|
>
|
|
|
|
|
|
<
|

1
2
3
4
5
6
7
8
9
10
11
12
13

14

15
16
17
18
19
20
21
22
23
24
25
26
27
28

29
30
31
32
33

34

35
36






37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

56
57
//-----------------------------------------------------------------------------
// Copyright (c) 2020-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 collect provides functions to collect items from a syntax tree.
package collect


import "zettelstore.de/z/ast"


// DivideReferences divides the given list of rederences into zettel, local, and external References.
func DivideReferences(all []*ast.Reference, duplicates bool) (zettel, local, external []*ast.Reference) {
	if len(all) == 0 {
		return nil, nil, nil
	}

	mapZettel := make(map[string]bool)
	mapLocal := make(map[string]bool)
	mapExternal := make(map[string]bool)
	for _, ref := range all {
		if ref.State == ast.RefStateSelf {
			continue
		}

		if ref.IsZettel() {
			zettel = appendRefToList(zettel, mapZettel, ref, duplicates)
		} else if ref.IsExternal() {
			external = appendRefToList(external, mapExternal, ref, duplicates)
		} else {

			local = appendRefToList(local, mapLocal, ref, duplicates)

		}
	}






	return zettel, local, external
}

func appendRefToList(
	reflist []*ast.Reference,
	refSet map[string]bool,
	ref *ast.Reference,
	duplicates bool,
) []*ast.Reference {
	if duplicates {
		reflist = append(reflist, ref)
	} else {
		s := ref.String()
		if _, ok := refSet[s]; !ok {
			reflist = append(reflist, ref)
			refSet[s] = true
		}
	}


	return reflist
}

Changes to config/runtime/runtime.go.

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// under this license.
//-----------------------------------------------------------------------------

// Package runtime provides functions to retrieve runtime configuration data.
package runtime

import (
	"strconv"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/stock"
)

// --- Configuration zettel --------------------------------------------------







<
<







8
9
10
11
12
13
14


15
16
17
18
19
20
21
// under this license.
//-----------------------------------------------------------------------------

// Package runtime provides functions to retrieve runtime configuration data.
package runtime

import (


	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/place/stock"
)

// --- Configuration zettel --------------------------------------------------
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
		if name, ok := config.Get(meta.KeySiteName); ok {
			return name
		}
	}
	return "Zettelstore"
}

// GetStart returns the value of the "start" key.
func GetStart() id.Zid {
	if config := getConfigurationMeta(); config != nil {
		if start, ok := config.Get(meta.KeyStart); ok {
			if startID, err := id.Parse(start); err == nil {
				return startID
			}
		}
	}
	return id.Invalid
}

// GetDefaultVisibility returns the default value for zettel visibility.
func GetDefaultVisibility() meta.Visibility {
	if config := getConfigurationMeta(); config != nil {
		if value, ok := config.Get(meta.KeyDefaultVisibility); ok {
			if vis := meta.GetVisibility(value); vis != meta.VisibilityUnknown {







|
|

|





|







128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
		if name, ok := config.Get(meta.KeySiteName); ok {
			return name
		}
	}
	return "Zettelstore"
}

// GetHomeZettel returns the value of the "home-zettel" key.
func GetHomeZettel() id.Zid {
	if config := getConfigurationMeta(); config != nil {
		if start, ok := config.Get(meta.KeyHomeZettel); ok {
			if startID, err := id.Parse(start); err == nil {
				return startID
			}
		}
	}
	return id.DefaultHomeZid
}

// GetDefaultVisibility returns the default value for zettel visibility.
func GetDefaultVisibility() meta.Visibility {
	if config := getConfigurationMeta(); config != nil {
		if value, ok := config.Get(meta.KeyDefaultVisibility); ok {
			if vis := meta.GetVisibility(value); vis != meta.VisibilityUnknown {
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
// GetMarkerExternal returns the current value of the "marker-external" key.
func GetMarkerExternal() string {
	if config := getConfigurationMeta(); config != nil {
		if html, ok := config.Get(meta.KeyMarkerExternal); ok {
			return html
		}
	}
	return "&#8599;&#xfe0e;"
}

// GetFooterHTML returns HTML code that should be embedded into the footer
// of each WebUI page.
func GetFooterHTML() string {
	if config := getConfigurationMeta(); config != nil {
		if data, ok := config.Get(meta.KeyFooterHTML); ok {
			return data
		}
	}
	return ""
}

// GetListPageSize returns the maximum length of a list to be returned in WebUI.
// A value less or equal to zero signals no limit.
func GetListPageSize() int {
	if config := getConfigurationMeta(); config != nil {
		if data, ok := config.Get(meta.KeyListPageSize); ok {
			if value, err := strconv.Atoi(data); err == nil {
				return value
			}
		}
	}
	return 0
}







|

















|
<
|
<




175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200

201

202
203
204
205
// GetMarkerExternal returns the current value of the "marker-external" key.
func GetMarkerExternal() string {
	if config := getConfigurationMeta(); config != nil {
		if html, ok := config.Get(meta.KeyMarkerExternal); ok {
			return html
		}
	}
	return "&#10138;"
}

// GetFooterHTML returns HTML code that should be embedded into the footer
// of each WebUI page.
func GetFooterHTML() string {
	if config := getConfigurationMeta(); config != nil {
		if data, ok := config.Get(meta.KeyFooterHTML); ok {
			return data
		}
	}
	return ""
}

// GetListPageSize returns the maximum length of a list to be returned in WebUI.
// A value less or equal to zero signals no limit.
func GetListPageSize() int {
	if config := getConfigurationMeta(); config != nil {
		if value, ok := config.GetNumber(meta.KeyListPageSize); ok && value > 0 {

			return value

		}
	}
	return 0
}

Changes to config/startup/startup.go.

100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
	h := fnv.New128()
	if secret, ok := cfg.Get("secret"); ok {
		io.WriteString(h, secret)
	}
	io.WriteString(h, version.Prog)
	io.WriteString(h, version.Build)
	io.WriteString(h, version.Hostname)
	io.WriteString(h, version.GoVersion)
	io.WriteString(h, version.Os)
	io.WriteString(h, version.Arch)
	return h.Sum(nil)
}

func getDuration(
	cfg *meta.Meta, key string, defDur, minDur, maxDur time.Duration) time.Duration {







<







100
101
102
103
104
105
106

107
108
109
110
111
112
113
	h := fnv.New128()
	if secret, ok := cfg.Get("secret"); ok {
		io.WriteString(h, secret)
	}
	io.WriteString(h, version.Prog)
	io.WriteString(h, version.Build)
	io.WriteString(h, version.Hostname)

	io.WriteString(h, version.Os)
	io.WriteString(h, version.Arch)
	return h.Sum(nil)
}

func getDuration(
	cfg *meta.Meta, key string, defDur, minDur, maxDur time.Duration) time.Duration {

Changes to docs/manual/00000000000100.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00000000000100
title: Zettelstore Runtime Configuration
role: configuration
syntax: none
default-copyright: (c) 2020-2021 by Detlef Stern <ds@zettelstore.de>
default-license: EUPL-1.2-or-later
default-visibility: public
footer-html: <hr><p><a href="/home/doc/trunk/www/impri.wiki">Imprint / Privacy</a></p>
modified: 20210111182407
site-name: Zettelstore Manual
start: 00001000000000
visibility: owner









<

<


1
2
3
4
5
6
7
8

9

10
11
id: 00000000000100
title: Zettelstore Runtime Configuration
role: configuration
syntax: none
default-copyright: (c) 2020-2021 by Detlef Stern <ds@zettelstore.de>
default-license: EUPL-1.2-or-later
default-visibility: public
footer-html: <hr><p><a href="/home/doc/trunk/www/impri.wiki">Imprint / Privacy</a></p>

site-name: Zettelstore Manual

visibility: owner

Deleted docs/manual/00001000000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id: 00001000000000
title: Zettelstore Manual
role: manual
tags: #manual #zettelstore
syntax: zmk
modified: 20210126174156

* [[Introduction|00001001000000]]
* [[Design goals|00001002000000]]
* [[Installation|00001003000000]]
* [[Configuration|00001004000000]]
* [[Structure of Zettelstore|00001005000000]]
* [[Layout of a zettel|00001006000000]]
* [[Zettelmarkup|00001007000000]]
* [[Other markup languages|00001008000000]]
* [[Security|00001010000000]]
* [[API|00001012000000]]
* [[Web user interface|00001014000000]]
* Troubleshooting
* Frequently asked questions

Licensed under the EUPL-1.2-or-later.
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































Changes to docs/manual/00001001000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001001000000
title: Introduction to the Zettelstore
role: manual
tags: #introduction #manual #zettelstore
syntax: zmk
modified: 20210126170856

[[Personal knowledge
management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] is
about collecting, classifying, storing, searching, retrieving, assessing,
evaluating, and sharing knowledge as a daily activity. Personal knowledge
management is done by most people, not necessarily as part of their main
business. It is essential for knowledge workers, like students, researchers,





<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001001000000
title: Introduction to the Zettelstore
role: manual
tags: #introduction #manual #zettelstore
syntax: zmk


[[Personal knowledge
management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] is
about collecting, classifying, storing, searching, retrieving, assessing,
evaluating, and sharing knowledge as a daily activity. Personal knowledge
management is done by most people, not necessarily as part of their main
business. It is essential for knowledge workers, like students, researchers,

Changes to docs/manual/00001002000000.zettel.


1
2
3
4
5
6
7

title: Design goals for the Zettelstore
tags: #design #goal #manual #zettelstore
syntax: zmk
role: manual

Zettelstore supports the following design goals:

>







1
2
3
4
5
6
7
8
id: 00001002000000
title: Design goals for the Zettelstore
tags: #design #goal #manual #zettelstore
syntax: zmk
role: manual

Zettelstore supports the following design goals:

Changes to docs/manual/00001003000000.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

title: Installation of the Zettelstore software
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
modified: 20201221142822

=== The curious user
You just want to check out the Zettelstore software

* Grab the appropriate executable and copy it into any directory
* Start the Zettelstore software, e.g. with a double click
* A sub-directory ""zettel"" will be created in the directory where you placed the executable.
  It will contain your future zettel.
* Open the URI [[http://localhost:23123]] with your web browser.
  It will present you a mostly empty Zettelstore.
  There will be a zettel titled ""Welcome to Zettelstore"" that contains some helpful information.
* Please read the instructions for the web-based user interface and learn about the various ways to write zettel.
* If you restart your device, please make sure to start your Zettelstore.

=== The intermediate user
You already tried the Zettelstore software and now you want to use it permanently.

* Grab the appropriate executable and copy it into the appropriate directory
* ...

>




<










|

|







1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
id: 00001003000000
title: Installation of the Zettelstore software
role: manual
tags: #installation #manual #zettelstore
syntax: zmk


=== The curious user
You just want to check out the Zettelstore software

* Grab the appropriate executable and copy it into any directory
* Start the Zettelstore software, e.g. with a double click
* A sub-directory ""zettel"" will be created in the directory where you placed the executable.
  It will contain your future zettel.
* Open the URI [[http://localhost:23123]] with your web browser.
  It will present you a mostly empty Zettelstore.
  There will be a zettel titled ""[[Home|00010000000000]]"" that contains some helpful information.
* Please read the instructions for the web-based user interface and learn about the various ways to write zettel.
* If you restart your device, please make sure to start your Zettelstore again.

=== The intermediate user
You already tried the Zettelstore software and now you want to use it permanently.

* Grab the appropriate executable and copy it into the appropriate directory
* ...

Changes to docs/manual/00001004000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id: 00001004000000
title: Configuration of Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20210125195740

There are two levels to change the behavior and/or the appearance of Zettelstore.
The first level is the configuration that is needed to start the services provided by Zettelstore.
For example, this includes the URI under which your Zettelstore is accessible.
* [[Zettelstore start-up configuration|00001004010000]]

The second level is configuring the running Zettelstore.
For example, you can configure the default language of your Zettelstore.
* [[Configure a running Zettelstore|00001004020000]]

The third level is the way to start Zettelstore services and to manage it.
* [[Command line parameters|00001004050000]]





<












1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
id: 00001004000000
title: Configuration of Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk


There are two levels to change the behavior and/or the appearance of Zettelstore.
The first level is the configuration that is needed to start the services provided by Zettelstore.
For example, this includes the URI under which your Zettelstore is accessible.
* [[Zettelstore start-up configuration|00001004010000]]

The second level is configuring the running Zettelstore.
For example, you can configure the default language of your Zettelstore.
* [[Configure a running Zettelstore|00001004020000]]

The third level is the way to start Zettelstore services and to manage it.
* [[Command line parameters|00001004050000]]

Changes to docs/manual/00001004010000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004010000
title: Zettelstore start-up configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20201226183537

The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some start-up options.
These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons.
For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are placed.
An attacker that is able to change the owner can do anything.
Therefore only the owner of the computer on which Zettelstore runs can change this information.






<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004010000
title: Zettelstore start-up configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk


The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some start-up options.
These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons.
For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are placed.
An attacker that is able to change the owner can do anything.
Therefore only the owner of the computer on which Zettelstore runs can change this information.

38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

  If ''true'', a persistent cookie is used.
  Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds.

  Default: ''false''
; [!place-X-uri]''place-//X//-uri'', where //X// is a number greater or equal to one
: Specifies a [[place|00001004011200]] where zettel are stored.
  During startup //X// is counted, starting with one, until no key is found.
  This allows to configure more than one place.

  If no ''place-1-uri'' key is given, the overall effect will be the same as if only ''place-1-uri'' was specified with the value ''dir://.zettel''.
  In this case, even a key ''place-2-uri'' will be ignored.
; [!read-only-mode]''read-only-mode''
: Puts the Zettelstore web service into a read-only mode.
  No changes are possible.
  Default: false.
; [!token-lifetime-api]''token-lifetime-api'', [!token-lifetime-html]''token-lifetime-html''
: Define lifetime of access tokens in minutes.
  Values are only valid if authentication is enabled, i.e. key ''owner'' is set.

  ''token-lifetime-api'' is for accessing Zettelstore via its API.
  Default: 10.

  ''token-lifetime-html'' specifies the lifetime for the HTML views.
  Default: 60.
  It is automatically extended, when a new HTML view is rendered.
; [!url-prefix]''url-prefix''
: Add the given string as a prefix to the local part of a Zettelstore local URL/URI when rendering zettel representations.
  Must start and end with a slash character (""''/''"", ''U+002F'').
  Default: ''"/"''.

  This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore.
; ''verbose''
: Be more verbose inf logging data.
  Default: false

Other keys will be ignored.







|




















|








37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

  If ''true'', a persistent cookie is used.
  Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds.

  Default: ''false''
; [!place-X-uri]''place-//X//-uri'', where //X// is a number greater or equal to one
: Specifies a [[place|00001004011200]] where zettel are stored.
  During start-up //X// is counted, starting with one, until no key is found.
  This allows to configure more than one place.

  If no ''place-1-uri'' key is given, the overall effect will be the same as if only ''place-1-uri'' was specified with the value ''dir://.zettel''.
  In this case, even a key ''place-2-uri'' will be ignored.
; [!read-only-mode]''read-only-mode''
: Puts the Zettelstore web service into a read-only mode.
  No changes are possible.
  Default: false.
; [!token-lifetime-api]''token-lifetime-api'', [!token-lifetime-html]''token-lifetime-html''
: Define lifetime of access tokens in minutes.
  Values are only valid if authentication is enabled, i.e. key ''owner'' is set.

  ''token-lifetime-api'' is for accessing Zettelstore via its API.
  Default: 10.

  ''token-lifetime-html'' specifies the lifetime for the HTML views.
  Default: 60.
  It is automatically extended, when a new HTML view is rendered.
; [!url-prefix]''url-prefix''
: Add the given string as a prefix to the local part of a Zettelstore local URL/URI when rendering zettel representations.
  Must begin and end with a slash character (""''/''"", ''U+002F'').
  Default: ''"/"''.

  This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore.
; ''verbose''
: Be more verbose inf logging data.
  Default: false

Other keys will be ignored.

Changes to docs/manual/00001004011200.zettel.


1
2
3
4
5
6
7

title: Zettelstore places
tags: #configuration #manual #zettelstore
syntax: zmk
role: manual

A Zettelstore must store its zettel somehow and somewhere.
In most cases you want to store your zettel as files in a directory.
>







1
2
3
4
5
6
7
8
id: 00001004011200
title: Zettelstore places
tags: #configuration #manual #zettelstore
syntax: zmk
role: manual

A Zettelstore must store its zettel somehow and somewhere.
In most cases you want to store your zettel as files in a directory.

Changes to docs/manual/00001004011400.zettel.


1
2
3
4
5
6
7

title: Configure file directory places
tags: #configuration #manual #zettelstore
syntax: zmk
role: manual

Under certain circumstances, it is preferable to further configure a file directory place.
This is done by appending query parameters after the base place URI ''dir:\//DIR''.
>







1
2
3
4
5
6
7
8
id: 00001004011400
title: Configure file directory places
tags: #configuration #manual #zettelstore
syntax: zmk
role: manual

Under certain circumstances, it is preferable to further configure a file directory place.
This is done by appending query parameters after the base place URI ''dir:\//DIR''.

Changes to docs/manual/00001004020000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20201231131204

You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]].
This zettel is called ""configuration zettel"".
The following metadata keys change the appearance / behavior of Zettelstore:

; [!default-copyright]''default-copyright''
: Copyright value to be used when rendering content.





<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk


You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]].
This zettel is called ""configuration zettel"".
The following metadata keys change the appearance / behavior of Zettelstore:

; [!default-copyright]''default-copyright''
: Copyright value to be used when rendering content.
45
46
47
48
49
50
51



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
  This affects most computed zettel.
  Default: False.
; [!footer-html]''footer-html''
: Contains some HTML code that will be included into the footer of each Zettelstore web page.
  It only affects the [[web user interface|00001014000000]].
  Zettel content, delivered via the [[API|00001012000000]] as JSON, etc. is not affected.
  Default: (the empty string).



; [!marker-external]''marker-external''
: Some HTML code that is displayed after a reference to external material.
  Default: ''&\#8599;&\#xfe0e;'', to display a ""&#8599;&#xfe0e;"" sign[^The string ''&\#xfe0e;'' is needed to enforce the sign on all platforms.].
; [!list-page-size]''list-page-size''
: If set to a value greater than zero, specifies the number of items shown in WebUI lists.
  Basically, this is the list of all zettel (possibly restricted) and the list of search results.
  Default: ''0''.
; [!site-name]''site-name''
: Name of the Zettelstore instance.
  Will be used when displaying some lists.
  Default: ''Zettelstore''.
; [!start]''start''
: Specifies the ID of the zettel, that should be presented for the default view.
  If not given or if the ID does not identify a zettel, the list of all zettel is shown.
; [!yaml-header]''yaml-header''
: If true, metadata and content will be separated by ''-\--\\n'' instead of an empty line (''\\n\\n'').
  Default: ''false''.

  You will probably use this key, if you are working with another software
  processing [[Markdown|https://daringfireball.net/projects/markdown/]] that
  uses a subset of [[YAML|https://yaml.org/]] to specify metadata.







>
>
>


|








<
<
<







44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64



65
66
67
68
69
70
71
  This affects most computed zettel.
  Default: False.
; [!footer-html]''footer-html''
: Contains some HTML code that will be included into the footer of each Zettelstore web page.
  It only affects the [[web user interface|00001014000000]].
  Zettel content, delivered via the [[API|00001012000000]] as JSON, etc. is not affected.
  Default: (the empty string).
; [!home-zettel]''home-zettel''
: Specifies the identifier of the zettel, that should be presented for the default view / home view.
  If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown.
; [!marker-external]''marker-external''
: Some HTML code that is displayed after a reference to external material.
  Default: ''&\#10138;'', to display a ""&#10138;"" sign.
; [!list-page-size]''list-page-size''
: If set to a value greater than zero, specifies the number of items shown in WebUI lists.
  Basically, this is the list of all zettel (possibly restricted) and the list of search results.
  Default: ''0''.
; [!site-name]''site-name''
: Name of the Zettelstore instance.
  Will be used when displaying some lists.
  Default: ''Zettelstore''.



; [!yaml-header]''yaml-header''
: If true, metadata and content will be separated by ''-\--\\n'' instead of an empty line (''\\n\\n'').
  Default: ''false''.

  You will probably use this key, if you are working with another software
  processing [[Markdown|https://daringfireball.net/projects/markdown/]] that
  uses a subset of [[YAML|https://yaml.org/]] to specify metadata.

Changes to docs/manual/00001004050000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004050000
title: Command line parameters
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210104115555

Zettelstore is not just a web service that provides services of a zettelkasten.
It allows to some tasks to be executed at the command line.
Typically, the task (""sub-command"") will be given at the command line as the first parameter.

If no parameter is given, the Zettelstore is called as
```





<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004050000
title: Command line parameters
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk


Zettelstore is not just a web service that provides services of a zettelkasten.
It allows to some tasks to be executed at the command line.
Typically, the task (""sub-command"") will be given at the command line as the first parameter.

If no parameter is given, the Zettelstore is called as
```

Changes to docs/manual/00001004050200.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004050200
title: The ''help'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210104115646
precursor: 00001004050000

Lists all implemented sub-commands.

Example:
```
# zettelstore help





<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004050200
title: The ''help'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

precursor: 00001004050000

Lists all implemented sub-commands.

Example:
```
# zettelstore help

Changes to docs/manual/00001004050400.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004050400
title: The ''version'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210104115659
precursor: 00001004050000

Emits some information about the Zettelstore's version.
This allows you to check, whether your installed Zettelstore is 

The name of the software (""Zettelstore"") and the build version information is given, as well as the compiler version, the name of the computer running the Zettelstore, and an indication about the operating system and the processor architecture of that computer.






<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004050400
title: The ''version'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

precursor: 00001004050000

Emits some information about the Zettelstore's version.
This allows you to check, whether your installed Zettelstore is 

The name of the software (""Zettelstore"") and the build version information is given, as well as the compiler version, the name of the computer running the Zettelstore, and an indication about the operating system and the processor architecture of that computer.

Changes to docs/manual/00001004050600.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004050600
title: The ''config'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210104115712
precursor: 00001004050000

Shows the Zettelstore configuration, for debugging purposes.
Currently, only the [[start-up configuration|00001004010000]] is shown.

This sub-command uses the same command line parameters as [[``zettelstore run``|00001004051000]].






<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004050600
title: The ''config'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

precursor: 00001004050000

Shows the Zettelstore configuration, for debugging purposes.
Currently, only the [[start-up configuration|00001004010000]] is shown.

This sub-command uses the same command line parameters as [[``zettelstore run``|00001004051000]].

Changes to docs/manual/00001004051000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004051000
title: The ''run'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210104115719
precursor: 00001004050000

=== ``zettelstore run``
This starts the web service.

```
zettelstore run [-c CONFIGFILE] [-d DIR] [-p PORT] [-r] [-v]





<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004051000
title: The ''run'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

precursor: 00001004050000

=== ``zettelstore run``
This starts the web service.

```
zettelstore run [-c CONFIGFILE] [-d DIR] [-p PORT] [-r] [-v]
38
39
40
41
42
43
44
45
46
47
; ''-r''
: Puts the Zettelstore in read-only mode.
  No changes are possible via the web interface / via the API.

  This allows to publish your content without any risks of unauthorized changes.
; ''-v''
: Be more verbose in writing logs.
  Writes the startup configuration to stderr.

Command line options take precedence over configuration file options.







|


37
38
39
40
41
42
43
44
45
46
; ''-r''
: Puts the Zettelstore in read-only mode.
  No changes are possible via the web interface / via the API.

  This allows to publish your content without any risks of unauthorized changes.
; ''-v''
: Be more verbose in writing logs.
  Writes the start-up configuration to stderr.

Command line options take precedence over configuration file options.

Changes to docs/manual/00001004051100.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004051100
title: The ''run-simple'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210104115448
precursor: 00001004050000

=== ``zettelstore run-simple``
This sub-command is implicitly called, when an user starts Zettelstore by double-clicking on its GUI icon.
It is s simplified variant of the [[''run'' sub-command|00001004051000]].

It allows only to specify a zettel directory.





<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004051100
title: The ''run-simple'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

precursor: 00001004050000

=== ``zettelstore run-simple``
This sub-command is implicitly called, when an user starts Zettelstore by double-clicking on its GUI icon.
It is s simplified variant of the [[''run'' sub-command|00001004051000]].

It allows only to specify a zettel directory.

Changes to docs/manual/00001004051200.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004051200
title: The ''file'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210104115726
precursor: 00001004050000

Reads zettel data from a file (or from standard input / stdin) and renders it to standard output / stdout.
This allows Zettelstore to render files manually.
```
zettelstore file [-t FORMAT] [file-1 [file-2]]
```





<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004051200
title: The ''file'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

precursor: 00001004050000

Reads zettel data from a file (or from standard input / stdin) and renders it to standard output / stdout.
This allows Zettelstore to render files manually.
```
zettelstore file [-t FORMAT] [file-1 [file-2]]
```

Changes to docs/manual/00001004051400.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004051400
title: The ''password'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20210104115737
precursor: 00001004050000

This sub-command is used to create a hashed password for to be authenticated users.

It reads a password from standard input (two times, both must be equal) and writes the hashed password to standard output.

The general usage is:





<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004051400
title: The ''password'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

precursor: 00001004050000

This sub-command is used to create a hashed password for to be authenticated users.

It reads a password from standard input (two times, both must be equal) and writes the hashed password to standard output.

The general usage is:

Changes to docs/manual/00001005000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
id: 00001005000000
title: Structure of Zettelstore
role: manual
tags: #design #manual #zettelstore
syntax: zmk
modified: 20210125195908

Zettelstore is a software that manages your zettel.
Since every zettel must be readable without any special tool, most zettel has to be stored as ordinary files within specific directories.
Typically, file names and file content must comply to specific rules so that Zettelstore can manage them.
If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions.

Zettelstore provides additional services to the user.
Via a builtin web interface you can work with zettel in various ways.
For example, you are able to list zettel, to create new zettel, to edit them, or to delete them.
You can view zettel details and relations between zettel.

In addition, Zettelstore provides an ""application programming interface"" (API) that allows other software to communicate with the Zettelstore.
Zettelstore becomes extensible by external software.
For example, a more sophisticated web interface could be build, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel.

=== Where zettel are stored

Your zettel are stored as files in a specific directory.
If you have not explicitly specified the directory, a default directory will be used.
The directory has to be specified at [[startup time|00001004010000]].
Nested directories are not supported (yet).

Every file in this directory that should be monitored by Zettelstore must have a file name that starts with 14 digits (0-9), the [[zettel identifier|00001006050000]].
If you create a new zettel via the web interface or the API, the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss'').
This allows zettel to be sorted naturally by creation time.

Since the only restriction on zettel identifiers are the 14 digits, you are free to use other digit sequences.
The [[configuration zettel|00001004020000]] is one prominent example, as well as these manual zettel.
You can create these special zettel identifiers either with the //rename// function of Zettelstore or by manually renaming the underlying zettel files.






<



















|


|







1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
id: 00001005000000
title: Structure of Zettelstore
role: manual
tags: #design #manual #zettelstore
syntax: zmk


Zettelstore is a software that manages your zettel.
Since every zettel must be readable without any special tool, most zettel has to be stored as ordinary files within specific directories.
Typically, file names and file content must comply to specific rules so that Zettelstore can manage them.
If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions.

Zettelstore provides additional services to the user.
Via a builtin web interface you can work with zettel in various ways.
For example, you are able to list zettel, to create new zettel, to edit them, or to delete them.
You can view zettel details and relations between zettel.

In addition, Zettelstore provides an ""application programming interface"" (API) that allows other software to communicate with the Zettelstore.
Zettelstore becomes extensible by external software.
For example, a more sophisticated web interface could be build, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel.

=== Where zettel are stored

Your zettel are stored as files in a specific directory.
If you have not explicitly specified the directory, a default directory will be used.
The directory has to be specified at [[start-up time|00001004010000]].
Nested directories are not supported (yet).

Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]].
If you create a new zettel via the web interface or the API, the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss'').
This allows zettel to be sorted naturally by creation time.

Since the only restriction on zettel identifiers are the 14 digits, you are free to use other digit sequences.
The [[configuration zettel|00001004020000]] is one prominent example, as well as these manual zettel.
You can create these special zettel identifiers either with the //rename// function of Zettelstore or by manually renaming the underlying zettel files.

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
It maintains this relationship as long as theses files exists.

In case of some textual zettel content you do not want to store the metadata and the zettel content in two different files.
Here the ''.zettel'' extension will signal that the metadata and the zettel content will be placed in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator"").

=== Predefined zettel

Zettelstore contains some predefined zettel to work properly.
The [[configuration zettel|00001004020000]] is one example.
To render the builtin web interface, some templates are used, as well as a layout specification in CSS.
The icon that visualizes an external link is a predefined SVG image.
All of these are visible to the Zettelstore as zettel.

One reason for this is to allow you to modify these zettel to adapt Zettelstore to your needs and visual preferences.








|







48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
It maintains this relationship as long as theses files exists.

In case of some textual zettel content you do not want to store the metadata and the zettel content in two different files.
Here the ''.zettel'' extension will signal that the metadata and the zettel content will be placed in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator"").

=== Predefined zettel

Zettelstore contains some [[predefined zettel|00001005090000]] to work properly.
The [[configuration zettel|00001004020000]] is one example.
To render the builtin web interface, some templates are used, as well as a layout specification in CSS.
The icon that visualizes an external link is a predefined SVG image.
All of these are visible to the Zettelstore as zettel.

One reason for this is to allow you to modify these zettel to adapt Zettelstore to your needs and visual preferences.

Changes to docs/manual/00001005090000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

33
34

35
36
37
38
id: 00001005090000
title: List of predefined zettel
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
modified: 20210126114739

The following table lists all predefined zettel with their purpose.

|= Identifier :|= Title | Purpose
| [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore
| [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore
| [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore
| [[00000000000006]] | Zettelstore Environment Values | Contains environmental data of Zettelstore executable
| [[00000000000008]] | Zettelstore Runtime Values | Contains values that reflect the inner working; see [[here|https://golang.org/pkg/runtime/]] for a technical description of these values
| [[00000000000018]] | Zettelstore Indexer | Provides some statistics about the index process
| [[00000000000020]] | Zettelstore Place Manager | Contains some statistics about zettel places
| [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more
| [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]]
| [[00000000000098]] | Zettelstore Startup Values | Contains all values computed from the [[startup configuration|00001004010000]]
| [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]]
| [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view
| [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]]
| [[00000000010300]] | Zettelstore List Meta HTML Template | Used when displaying a list of zettel
| [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel
| [[00000000010402]] | Zettelstore Info HTML Templöate | Layout for the information view of a specific zettel
| [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text
| [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]]
| [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel
| [[00000000010500]] | Zettelstore List Roles HTML Template | Layout for listing all roles
| [[00000000010600]] | Zettelstore List Tags HTML Template | Layout of tags lists
| [[00000000020001]] | Zettelstore Base CSS | CSS file that is included by the [[Base HTML Template|00000000010100]]

| [[00000000091001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]""
| [[00000000096001]] | New User | Template for a new zettel with role ""[[user|00001006020100#user]]""


If a zettel is not linked, it is not accessible for the current user.

**Important:** The identifier may change until a stable version of the software is released.





<












|
|












>
|
|
>




1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
id: 00001005090000
title: List of predefined zettel
role: manual
tags: #manual #reference #zettelstore
syntax: zmk


The following table lists all predefined zettel with their purpose.

|= Identifier :|= Title | Purpose
| [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore
| [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore
| [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore
| [[00000000000006]] | Zettelstore Environment Values | Contains environmental data of Zettelstore executable
| [[00000000000008]] | Zettelstore Runtime Values | Contains values that reflect the inner working; see [[here|https://golang.org/pkg/runtime/]] for a technical description of these values
| [[00000000000018]] | Zettelstore Indexer | Provides some statistics about the index process
| [[00000000000020]] | Zettelstore Place Manager | Contains some statistics about zettel places
| [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more
| [[00000000000096]] | Zettelstore Start-up Configuration | Contains the effective values of the [[start-up configuration|00001004010000]]
| [[00000000000098]] | Zettelstore Start-up Values | Contains all values computed from the [[start-up configuration|00001004010000]]
| [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]]
| [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view
| [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]]
| [[00000000010300]] | Zettelstore List Meta HTML Template | Used when displaying a list of zettel
| [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel
| [[00000000010402]] | Zettelstore Info HTML Templöate | Layout for the information view of a specific zettel
| [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text
| [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]]
| [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel
| [[00000000010500]] | Zettelstore List Roles HTML Template | Layout for listing all roles
| [[00000000010600]] | Zettelstore List Tags HTML Template | Layout of tags lists
| [[00000000020001]] | Zettelstore Base CSS | CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu
| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]""
| [[00000000090002]] | New User | Template for a new zettel with role ""[[user|00001006020100#user]]""
| [[00010000000000]] | Home | Default home zettel, contains some welcome information

If a zettel is not linked, it is not accessible for the current user.

**Important:** The identifier may change until a stable version of the software is released.

Changes to docs/manual/00001006000000.zettel.


1
2
3
4
5
6
7

title: Layout of a Zettel
tags: #design #manual #zettelstore
syntax: zmk
role: manual

A zettel consists of two part: the metadata and the zettel content.
Metadata gives some information mostly about the zettel content, how it should be interpreted, how it is sorted within Zettelstore.
>







1
2
3
4
5
6
7
8
id: 00001006000000
title: Layout of a Zettel
tags: #design #manual #zettelstore
syntax: zmk
role: manual

A zettel consists of two part: the metadata and the zettel content.
Metadata gives some information mostly about the zettel content, how it should be interpreted, how it is sorted within Zettelstore.

Changes to docs/manual/00001006010000.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

title: Syntax of Metadata
tags: #manual #syntax #zettelstore
syntax: zmk
role: manual

The metadata of a zettel is a collection of key-value pairs.
The syntax roughly resembles the internal header of an email ([[RFC5322|https://tools.ietf.org/html/rfc5322]]).

The key is a sequence of alphanumeric characters, a hyphen-minus character (""''-''"") is also allowed.
It starts at a new line.

A key is separated from its value either by
* a colon character (""'':''""), 
* a non-empty sequence of space characters, 
* a sequence of space characters, followed by a colon, followed by a sequence of space characters.

A Value is a sequence of printable characters.
If the value should be continued in the following line, that following line (//continuation line//) must start with a non-empty sequence of space characters.
The rest of the following line will be interpreted as the next part of the value.
There can be more than one continuation line for a value.

A non-continuation line that contains a possibly empty sequence of characters, followed by the percent sign character (""''%''"") is treated as a comment line.
It will be ignored.

Parsing metadata ends, if an empty line is found or if a line with at least three hyphen-minus characters is found.
>









|


|
|



|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
id: 00001006010000
title: Syntax of Metadata
tags: #manual #syntax #zettelstore
syntax: zmk
role: manual

The metadata of a zettel is a collection of key-value pairs.
The syntax roughly resembles the internal header of an email ([[RFC5322|https://tools.ietf.org/html/rfc5322]]).

The key is a sequence of alphanumeric characters, a hyphen-minus character (""''-''"") is also allowed.
It begins at the first position of a new line.

A key is separated from its value either by
* a colon character (""'':''""),
* a non-empty sequence of space characters,
* a sequence of space characters, followed by a colon, followed by a sequence of space characters.

A Value is a sequence of printable characters.
If the value should be continued in the following line, that following line (//continuation line//) must begin with a non-empty sequence of space characters.
The rest of the following line will be interpreted as the next part of the value.
There can be more than one continuation line for a value.

A non-continuation line that contains a possibly empty sequence of characters, followed by the percent sign character (""''%''"") is treated as a comment line.
It will be ignored.

Parsing metadata ends, if an empty line is found or if a line with at least three hyphen-minus characters is found.

Changes to docs/manual/00001006020000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001006020000
title: Supported Metadata Keys
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
modified: 20210123223645

Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore.
See the [[computed list of supported metadata keys|00000000000090]] for details.

Most keys conform to a [[type|00001006030000]].

; [!back]''back''





<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001006020000
title: Supported Metadata Keys
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk


Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore.
See the [[computed list of supported metadata keys|00000000000090]] for details.

Most keys conform to a [[type|00001006030000]].

; [!back]''back''
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
  If not given, the value ''default-role'' from the [[configuration zettel|00001004020000#default-role]] will be used.
; [!syntax]''syntax''
: Specifies the syntax that should be used for interpreting the zettel.
  The zettel about [[other markup languages|00001008000000]] defines supported values.
  If not given, the value ''default-syntax'' from the [[configuration zettel|00001004020000#default-syntax]] will be used.
; [!tags]''tags''
: Contains a space separated list of tags to describe the zettel further.
  Each Tag must start with the number sign character (""''#''"", ''U+0023'').
; [!title]''title''
: Specifies the title of the zettel.
  If not given, the value ''default-title'' from the [[configuration zettel|00001004020000#default-title]] will be used.

  You can use all [[inline-structured elements|00001007040000]] of Zettelmarkup.
; [!url]''url''
: Defines an URL / URI for this zettel that possibly references external material.







|







71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
  If not given, the value ''default-role'' from the [[configuration zettel|00001004020000#default-role]] will be used.
; [!syntax]''syntax''
: Specifies the syntax that should be used for interpreting the zettel.
  The zettel about [[other markup languages|00001008000000]] defines supported values.
  If not given, the value ''default-syntax'' from the [[configuration zettel|00001004020000#default-syntax]] will be used.
; [!tags]''tags''
: Contains a space separated list of tags to describe the zettel further.
  Each Tag must begin with the number sign character (""''#''"", ''U+0023'').
; [!title]''title''
: Specifies the title of the zettel.
  If not given, the value ''default-title'' from the [[configuration zettel|00001004020000#default-title]] will be used.

  You can use all [[inline-structured elements|00001007040000]] of Zettelmarkup.
; [!url]''url''
: Defines an URL / URI for this zettel that possibly references external material.
97
98
99
100
101
102
103
104
105
106
107
108
109

  See [[User roles|00001010070300]] for more details.
; [!visibility]''visibility''
: When you work with authentication, you can give every zettel a value to decide, who can see the zettel.
  Its default value can be set with [[''default-visibility''|00001004020000#default-visibility]] of the configuration zettel.

  See [[visibility rules for zettel|00001010070200]] for more details.

---
Not yet supported, but planned:

; [!folge]''folge''
: The IDs of zettel that acts as a [[Folgezettel|https://zettelkasten.de/posts/tags/folgezettel/]].







<
<
<
<
<
<
96
97
98
99
100
101
102







  See [[User roles|00001010070300]] for more details.
; [!visibility]''visibility''
: When you work with authentication, you can give every zettel a value to decide, who can see the zettel.
  Its default value can be set with [[''default-visibility''|00001004020000#default-visibility]] of the configuration zettel.

  See [[visibility rules for zettel|00001010070200]] for more details.






Changes to docs/manual/00001006020100.zettel.


1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

title: Supported Zettel Roles

tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
role: manual

The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing.
The following values are used by Zettelstore:

; [!new-template]''new-template''
: Zettel with this role are used as templates for creating new zettel.
  Within such a zettel, the metadata key [[''new-role''|00001006020000#new-role]] is used to specify the role of the new zettel.
; [!user]''user''
: If you want to use [[authentication|00001010000000]], all zettel that identify users of the zettel store must have this role.

Beside this, you are free to define your own roles.

The role ''zettel'' is predefined as the default role, but you can [[change this|00001004020000#default-role]].

Some roles are defined for technical reasons:

; [!configuration]''configuration''
: A zettel that contains some configuration data for the Zettelstore.
>

>


<


|

<
<
<



|







1
2
3
4
5

6
7
8
9



10
11
12
13
14
15
16
17
18
19
20
id: 00001006020100
title: Supported Zettel Roles
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk


The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing.
The following values are used internally by Zettelstore and must exist:




; [!user]''user''
: If you want to use [[authentication|00001010000000]], all zettel that identify users of the zettel store must have this role.

Beside of this, you are free to define your own roles.

The role ''zettel'' is predefined as the default role, but you can [[change this|00001004020000#default-role]].

Some roles are defined for technical reasons:

; [!configuration]''configuration''
: A zettel that contains some configuration data for the Zettelstore.

Changes to docs/manual/00001006020400.zettel.


1
2
3
4
5
6

7

8
9
10
11

12
13

14

15
16
17

18
19

20
21
22
23
24
25
26
27

28
29

30

title: Supported values for metadata key ''read-only''
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

A zettel can be marked as read-only, if it contains a metadata value for key [[''read-only''|00001006020000#read-only]].

If user authentication is [[enabled|00001010040100]], it is possible to allow some users to change the zettel, depending on their [[user role|00001010070300]].

Otherwise, the read-only mark is just a binary value.

=== No authentication
If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030000]] is interpreted as ""false"", anybody can modify the zettel.


If the metadata value is something else (the value ""true"" is recommended), the user cannot modify the zettel through the web interface.

However, if the zettel is stored as a file in a [[directory place|00001004011400]], the zettel could be modified using an external editor.


=== Authentication enabled
If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030000]] is interpreted as ""false"", anybody can modify the zettel.


If the metadata value is the same as an explicit [[user role|00001010070300]], user with that role (or below) are not allowed to modify the zettel.


; ''reader''
: Neither an unauthenticated user nor a user with role ""reader"" is allowed to modify the zettel.
  Users with role ""writer"" or the owner itself still can modify the zettel.
; ''writer''
: Neither an unauthenticated user, nor users with roles ""reader"" or ""writer"" are allowed to modify the zettel.
  Only the owner of the Zettelstore can modify the zettel.


If the metadata value is something else (the value ""owner"" is recommended), no user is allowed modify the zettel through the web interface.
However, if the zettel is accessible as a file in a [[directory place|00001004011400]], the zettel could be modified using an external editor.

Typically the owner of a Zettelstore should have such an access.
>





|
>
|
>



|
>

|
>
|
>


|
>

|
>

|


|



>
|
|
>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
id: 00001006020400
title: Supported values for metadata key ''read-only''
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

A zettel can be marked as read-only, if it contains a metadata value for key
[[''read-only''|00001006020000#read-only]].
If user authentication is [[enabled|00001010040100]], it is possible to allow some users to change the zettel,
depending on their [[user role|00001010070300]].
Otherwise, the read-only mark is just a binary value.

=== No authentication
If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]]
is interpreted as ""false"", anybody can modify the zettel.

If the metadata value is something else (the value ""true"" is recommended),
the user cannot modify the zettel through the web interface.
However, if the zettel is stored as a file in a [[directory place|00001004011400]],
the zettel could be modified using an external editor.

=== Authentication enabled
If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]]
is interpreted as ""false"", anybody can modify the zettel.

If the metadata value is the same as an explicit [[user role|00001010070300]],
users with that role (or a role with lower rights) are not allowed to modify the zettel.

; ""reader""
: Neither an unauthenticated user nor a user with role ""reader"" is allowed to modify the zettel.
  Users with role ""writer"" or the owner itself still can modify the zettel.
; ""writer""
: Neither an unauthenticated user, nor users with roles ""reader"" or ""writer"" are allowed to modify the zettel.
  Only the owner of the Zettelstore can modify the zettel.

If the metadata value is something else (one of the values ""true"" or ""owner"" is recommended),
no user is allowed modify the zettel through the web interface.
However, if the zettel is accessible as a file in a [[directory place|00001004011400]],
the zettel could be modified using an external editor.
Typically the owner of a Zettelstore have such an access.

Changes to docs/manual/00001006030000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12

13
14
15
16
17
18
19
20
21
22
23
24

25
26



27
28
29
30
31
id: 00001006030000
title: Supported Key Types
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
modified: 20210108184053

Most [[supported metadata keys|00001006020000]] conform to a type.

Every metadata key should conform to a type.
Every key type is specified by a letter.
User-defined types are normally strings (type ''e'').


Every key type has an associated validation rule to check values of the given
type. There is also a rule how values are matched, e.g. against a search term
when selecting some zettel. And there is a rule, how values compare for
sorting.

|= Name | Meaning | Match | Sorting
| Boolean | Boolean value, False if value starts with ""''0''"", ""''F''"", ""''N''"", ""''f''"", or ""''n''"" | Boolean match | False < True
| Credential | Value is a credential, e.g. an encrypted password (planned) | Never matches | Uses zettel identifier for sorting
| Timestamp | Timestamp value YYYYMMDDHHmmSS | prefix match | by number
| EString | Any string, possibly empty | case-insensitive contains | case-sensitive
| Identifier | Value is a [[zettel identifier|00001006050000]] | prefix match | by number

| Number | Integer value | exact match | by number
| String | Any string, must not be empty | case-insensitive contains | case-sensitive



| TagSet | Value is a space-separated list of tags | exact match for one tag | case sensitive by sorted tags
| Word | Alfanumeric word, case-insensitive | case-insensitive equality | case-sensitive
| WordSet | Space-separated list of alfanumeric words, case-insensitive | case-insensitive match for one word | case-sensitive by sorted words
| URL | URL / URI | case-insensitive contains | case-sensitive
| Zettelmarkup | Any string, must not be empty, formatted in [[Zettelmarkup|00001007000000]] | case-insensitive contains | case-sensitive





<




<
|
>

|
|
|
<

|
<
|
<
|
|
>
|
|
>
>
>
|
|
<
<
|
1
2
3
4
5

6
7
8
9

10
11
12
13
14
15

16
17

18

19
20
21
22
23
24
25
26
27
28


29
id: 00001006030000
title: Supported Key Types
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk


Most [[supported metadata keys|00001006020000]] conform to a type.

Every metadata key should conform to a type.

User-defined metadata keys are of type EString.
The name of the metadata key is bound to the key type

Every key type has an associated validation rule to check values of the given type.
There is also a rule how values are matched, e.g. against a search term when selecting some zettel.
And there is a rule, how values compare for sorting.


* [[Boolean|00001006030500]]

* [[Credential|00001006031000]]

* [[EString|00001006031500]]
* [[Identifier|00001006032000]]
* [[IdentifierSet|00001006032500]]
* [[Number|00001006033000]]
* [[String|00001006033500]]
* [[TagSet|00001006034000]]
* [[Timestamp|00001006034500]]
* [[URL|00001006035000]]
* [[Word|00001006035500]]
* [[WordSet|00001006036000]]


* [[Zettelmarkup|00001006036500]]

Added docs/manual/00001006030500.zettel.











































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
id: 00001006030500
title: Boolean Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type denote a truth value.

=== Allowed values
Every character sequence that begins with a ""''0''"", ""''F''"", ""''N''"", ""''f''"", or a ""''n''"" is interpreted as the ""false"" boolean value.
All other metadata value is interpreted as the ""true"" boolean value.

=== Match operator
The match operator is the equals operator, i.e.
* ``(true == true) == true``
* ``(false == false) == true``
* ``(true == false) == false``
* ``(false == true) == false``

=== Sorting
The ""false"" value is less than the ""true"" value: ``false < true``

Added docs/manual/00001006031000.zettel.



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
id: 00001006031000
title: Credential Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type denote a credential value, e.g. an encrypted password.

=== Allowed values
All printable characters are allowed.
Since a credential contains some kind of secret, the sequence of characters might have some hidden syntax to be interpreted by other parts of Zettelstore.

=== Match operator
A credential never matches to any other value.

=== Sorting
If a list of zettel should be sorted based on a credential value, the identifier of the respective zettel is used instead.

Added docs/manual/00001006031500.zettel.





















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
id: 00001006031500
title: EString Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type are just a sequence of character, possibly an empty sequence.

An EString is the most general metadata key type, as it places no restrictions to the character sequence.[^Well, there are some minor restrictions that follow from the [[metadata syntax|00001006010000]].]

=== Allowed values
All printable characters are allowed.

=== Match operator
A value matches an EString value, if the first value is part of the EString value.
This check is done case-insensitive.

For example, ""hell"" matches ""Hello"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.

Added docs/manual/00001006032000.zettel.









































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001006032000
title: Identifier Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type denote a [[zettel identifier|00001006050000]].

=== Allowed values
Must be a sequence of 14 digits (""0""--""9"").

=== Match operator
A value matches an identifier value, if the first value is the prefix of the identifier value.

For example, ""000010"" matches ""[[00001006032000]]"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

If both values are identifiers, this works well because both have the same length.

Added docs/manual/00001006032500.zettel.









































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001006032500
title: IdentifierSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type denote a (sorted) set of [[zettel identifier|00001006050000]].

A set is different to a list, as no duplicates are allowed.

=== Allowed values
Must be at least one sequence of 14 digits (""0""--""9""), separated by space characters.

=== Match operator
A value matches an identifier set value, if the first value is a prefix of one of the identifier value.

For example, ""000010"" matches ""[[00001006032000]] [[00001006032500]]"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

Added docs/manual/00001006033000.zettel.





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id: 00001006033000
title: Number Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type denote a numeric integer value.

=== Allowed values
Must be a sequence of digits (""0""--""9""), optionally prefixed with a ""-"" or a ""+"" character.

=== Match operator
The match operator is the equals operator, i.e. two values must be numeric equal to match.

This includes that ""+12"" is equal to ""12"", therefore both values match.

=== Sorting
Sorting is done by comparing the numeric values.

Added docs/manual/00001006033500.zettel.



















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
id: 00001006033500
title: String Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type are just a sequence of character, but not an empty sequence.

=== Allowed values
All printable characters are allowed.
There must be at least one such character.

=== Match operator
A value matches a String value, if the first value is part of the String value.
This check is done case-insensitive.

For example, ""hell"" matches ""Hello"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.

Added docs/manual/00001006034000.zettel.







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id: 00001006034000
title: TagSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type denote a (sorted) set of tags.

A set is different to a list, as no duplicates are allowed.

=== Allowed values
Every tag must must begin with the number sign character (""''#''"", ''U+0023''), followed by at least one printable character.
Tags are separated by space characters.

=== Match operator
A value matches a tag set value, if the first value is equal to at least one tag in the tag set.

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

Added docs/manual/00001006034500.zettel.























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
id: 00001006034500
title: Timestamp Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type denote a point in time.

=== Allowed values
Must be a sequence of 14 digits (""0""--""9"") (same as an [[Identifier|00001006032000]]), with the restriction that is conforms to the pattern ""YYYYMMDDhhmmss"".

* YYYY is the year,
* MM is the month,
* DD is the day,
* hh is the hour,
* mm is the minute,
* ss is the second.

=== Match operator
A value matches an timestampvalue, if the first value is the prefix of the timestamp value.

For example, ""202102"" matches ""20210212143200"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

If both values are timestamp values, this works well because both have the same length.

Added docs/manual/00001006035000.zettel.







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id: 00001006035000
title: URL Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type denote an URL.

=== Allowed values
All characters of an URL / URI are allowed.

=== Match operator
A value matches a URL value, if the first value is part of the URL value.
This check is done case-insensitive.

For example, ""hell"" matches ""http://example.com/Hello"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

Added docs/manual/00001006035500.zettel.

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id: 00001006035500
title: Word Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type denote a single word.

=== Allowed values
Must be a non-empty sequence of characters, but without the space character.

=== Match operator
A value matches a word value, if both value are character-wise equal.

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

Added docs/manual/00001006036000.zettel.





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id: 00001006036000
title: WordSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type denote a (sorted) set of [[words|00001006035500]].

A set is different to a list, as no duplicates are allowed.

=== Allowed values
Must be a sequence of at least one word, separated by space characters.

=== Match operator
A value matches an wordset value, if the first value is equal to one of the word values in the word set.

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

Added docs/manual/00001006036500.zettel.



















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
id: 00001006036500
title: Zettelmarkup Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

Values of this type are [[String|00001006033500]] values, interpreted as [[Zettelmarkup|00001007000000]].

=== Allowed values
All printable characters are allowed.
There must be at least one such character.

=== Match operator
A value matches a String value, if the first value is part of the String value.
This check is done case-insensitive.

For example, ""hell"" matches ""Hello"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.

Changes to docs/manual/00001006050000.zettel.


1
2
3
4
5
6
7
8
9
10
11

12
13
14
15
16
17
18

19

20
21
22


title: Zettel identifier
tags: #design #manual #zettelstore
syntax: zmk
role: manual

Each zettel is given a unique identifier.
To some degree, the zettel identifier is part of the metadata.
Basically, the identifier is given by the [[Zettelstore|00001005000000]] software.

Every zettel identifier consists of 14 digits.
They resemble a timestamp: the first four digits could represent the year, the next two represent the month, following by day, hour, minute, and second.


This allows to order zettel chronologically in a canonical way.

In most cases the zettel identifier is the timestamp when the zettel was created.

However, the Zettelstore software just checks for exactly 14 digits.
Anybody is free to assign a ""non-timestamp"" identifier to a zettel, e.g. with a month part of ""35"" or with ""99"" as the last two digits.

In fact, all identifiers of zettel initially provided by an empty Zettelstore begin with ""000000"".

The identifiers of zettel if this manual have be chosen to start with ""000010"".

A zettel can have any identifier that contains 14 digits and that is not in use by another zettel managed by the same Zettelstore.

>










|
>






|
>
|
>
|

|
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
id: 00001006050000
title: Zettel identifier
tags: #design #manual #zettelstore
syntax: zmk
role: manual

Each zettel is given a unique identifier.
To some degree, the zettel identifier is part of the metadata.
Basically, the identifier is given by the [[Zettelstore|00001005000000]] software.

Every zettel identifier consists of 14 digits.
They resemble a timestamp: the first four digits could represent the year, the
next two represent the month, following by day, hour, minute, and second.

This allows to order zettel chronologically in a canonical way.

In most cases the zettel identifier is the timestamp when the zettel was created.

However, the Zettelstore software just checks for exactly 14 digits.
Anybody is free to assign a ""non-timestamp"" identifier to a zettel, e.g. with
a month part of ""35"" or with ""99"" as the last two digits.
In fact, all identifiers of zettel initially provided by an empty Zettelstore
begin with ""000000"", except the home zettel ''00010000000000''.
The identifiers of zettel if this manual have be chosen to begin with ""000010"".

A zettel can have any identifier that contains 14 digits and that is not in use
by another zettel managed by the same Zettelstore.

Changes to docs/manual/00001007000000.zettel.


1
2
3
4
5
6
7

title: Zettelmarkup
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Zettelmarkup is a rich plain-text based markup language for writing zettel content.
Besides the zettel content, Zettelmarkup is also used for specifying the title of a zettel, regardless of the syntax of a zettel.
>







1
2
3
4
5
6
7
8
id: 00001007000000
title: Zettelmarkup
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Zettelmarkup is a rich plain-text based markup language for writing zettel content.
Besides the zettel content, Zettelmarkup is also used for specifying the title of a zettel, regardless of the syntax of a zettel.

Changes to docs/manual/00001007010000.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

title: Zettelmarkup: General Principles
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Any document can be thought as a sequence of paragraphs and other blocks-structural elements (""blocks""), such as headings, lists, quotations, and code blocks.
Some of these blocks can contain other blocks, for example lists may contain other lists or paragraphs.
Other blocks contain inline-structural elements (""inlines""), such as text, links, emphasized text, and images.

With the exception of lists and tables, the markup for blocks always starts at the first position of a line and starts with three or more identical characters.
List blocks also starts at the first position of a line, but may need one or more character, plus a space character.
Table blocks starts at the first position of a line with the character ""``|``"".
Non-list blocks are either fully specified on that line or they span multiple lines and are delimited with the same three or more character.
It depends on the block kind, whether blocks are specified on one line or on at least two lines.

If a line does not start with an explicit block element. the line is treated as a (implicit) paragraph block element that contains inline elements.
This paragraph ends when a block element is detected at the beginning of a next line or when an empty line occurs.
Some blocks may also contain inline elements, e.g. a heading.

Inline elements mostly starts with two non-space, often identical characters.
With some exceptions, two identical non-space characters starts a formatting range that is ended with the same two characters.

Exceptions are: links, images, edits, comments, and both the ""en-dash"" and the ""horizontal ellipsis"".
A link is given with ``[[...]]``{=zmk}, an images with ``{{...}}``{=zmk}, and an edit formatting with ``((...))``{=zmk}.
An inline comment, starting with the sequence ``%%``{=zmk}, always ends at the end of the line where it begins.
The ""en-dash"" (""--"") is specified as ``--``{=zmk}, the ""horizontal ellipsis"" (""..."") as ``...``{=zmk}[^If placed at the end of non-space text.].

Some inline elements do not follow the rule of two identical character, especially to specify footnotes, citation keys, and local marks.
These elements start with one opening square bracket (""``[``""), use a character for specifying the kind of the inline, typically allow to specify some content, and end with one closing square bracket (""``]``"").

One inline element that does not start with two characters is the ""entity"".
It allows to specify any Unicode character.
The specification of that character is placed between an ampersand character and a semicolon: ``&...;``{=zmk}.
For exmple, an ""n-dash"" could also be specified as ``&ndash;``{==zmk}.

The backslash character (""``\\``"") possibly gives the next character a special meaning.
This allows to resolve some left ambiguities.
For example, list of depth 2 will start a line with ``** Item 2.2``{=zmk}.
An inline element to strongly emphasize some text start with a space will be specified as ``** Text**``{=zmk}.
To force the inline element formatting at the start of a line, ``**\\ Text**``{=zmk} should better be specified.

Many block and inline elements can be refined by additional attributes.
Attributes resemble roughly HTML attributes and are placed near the corresponding elements by using the syntax ``{...}``{=zmk}.
One example is to make space characters visible inside a inline literal element: ``1 + 2 = 3``{-} was specified by using the default attribute: ``\`\`1 + 2 = 3\`\`{-}``.

To summarize:

* With some exceptions, blocks-structural elements starts at the for position of a line with three identical characters.
* The most important exception to this rule is the specification of lists.
* If no block element is found, a paragraph with inline elements is assumed.
* With some exceptions, inline-structural elements starts with two characters, quite often the same two characters.
* The most important exceptions are links.
* The backslash character can help to resolve possible ambiguities.
* Attributes refine some block and inline elements.
* Block elements have a higher priority than inline elements.

These principles makes automatic recognizing zettelmarkup an (relatively) easy task.
By looking at the reference implementation, a moderately skilled software developer should be able to create a appropriate software in a different programming language.
>









|
|
|



|



|
|



|



|

|






|
|
|







|


|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
id: 00001007010000
title: Zettelmarkup: General Principles
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Any document can be thought as a sequence of paragraphs and other blocks-structural elements (""blocks""), such as headings, lists, quotations, and code blocks.
Some of these blocks can contain other blocks, for example lists may contain other lists or paragraphs.
Other blocks contain inline-structural elements (""inlines""), such as text, links, emphasized text, and images.

With the exception of lists and tables, the markup for blocks always begins at the first position of a line with three or more identical characters.
List blocks also begins at the first position of a line, but may need one or more character, plus a space character.
Table blocks begins at the first position of a line with the character ""``|``"".
Non-list blocks are either fully specified on that line or they span multiple lines and are delimited with the same three or more character.
It depends on the block kind, whether blocks are specified on one line or on at least two lines.

If a line does not begin with an explicit block element. the line is treated as a (implicit) paragraph block element that contains inline elements.
This paragraph ends when a block element is detected at the beginning of a next line or when an empty line occurs.
Some blocks may also contain inline elements, e.g. a heading.

Inline elements mostly begins with two non-space, often identical characters.
With some exceptions, two identical non-space characters begins a formatting range that is ended with the same two characters.

Exceptions are: links, images, edits, comments, and both the ""en-dash"" and the ""horizontal ellipsis"".
A link is given with ``[[...]]``{=zmk}, an images with ``{{...}}``{=zmk}, and an edit formatting with ``((...))``{=zmk}.
An inline comment, beginning with the sequence ``%%``{=zmk}, always ends at the end of the line where it begins.
The ""en-dash"" (""--"") is specified as ``--``{=zmk}, the ""horizontal ellipsis"" (""..."") as ``...``{=zmk}[^If placed at the end of non-space text.].

Some inline elements do not follow the rule of two identical character, especially to specify footnotes, citation keys, and local marks.
These elements begin with one opening square bracket (""``[``""), use a character for specifying the kind of the inline, typically allow to specify some content, and end with one closing square bracket (""``]``"").

One inline element that does not begin with two characters is the ""entity"".
It allows to specify any Unicode character.
The specification of that character is placed between an ampersand character and a semicolon: ``&...;``{=zmk}.
For exmple, an ""n-dash"" could also be specified as ``&ndash;``{==zmk}.

The backslash character (""``\\``"") possibly gives the next character a special meaning.
This allows to resolve some left ambiguities.
For example, a list of depth 2 will begin a line with ``** Item 2.2``{=zmk}.
An inline element to strongly emphasize some text begin with a space will be specified as ``** Text**``{=zmk}.
To force the inline element formatting at the beginning of a line, ``**\\ Text**``{=zmk} should better be specified.

Many block and inline elements can be refined by additional attributes.
Attributes resemble roughly HTML attributes and are placed near the corresponding elements by using the syntax ``{...}``{=zmk}.
One example is to make space characters visible inside a inline literal element: ``1 + 2 = 3``{-} was specified by using the default attribute: ``\`\`1 + 2 = 3\`\`{-}``.

To summarize:

* With some exceptions, blocks-structural elements begins at the for position of a line with three identical characters.
* The most important exception to this rule is the specification of lists.
* If no block element is found, a paragraph with inline elements is assumed.
* With some exceptions, inline-structural elements begins with two characters, quite often the same two characters.
* The most important exceptions are links.
* The backslash character can help to resolve possible ambiguities.
* Attributes refine some block and inline elements.
* Block elements have a higher priority than inline elements.

These principles makes automatic recognizing zettelmarkup an (relatively) easy task.
By looking at the reference implementation, a moderately skilled software developer should be able to create a appropriate software in a different programming language.

Changes to docs/manual/00001007020000.zettel.


1
2
3
4
5
6
7

title: Zettelmarkup: Basic Definitions
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Every zettelmark content consists of a sequence of Unicode codepoints.
Unicode codepoints are called in the following as **character**s.
>







1
2
3
4
5
6
7
8
id: 00001007020000
title: Zettelmarkup: Basic Definitions
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Every zettelmark content consists of a sequence of Unicode codepoints.
Unicode codepoints are called in the following as **character**s.

Changes to docs/manual/00001007030000.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13

title: Zettelmarkup: Blocks-Structured Elements
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Every markup for blocks-structured elements (""blocks"") starts at the very first position of a line.

There are five kinds of blocks: lists, one-line blocks, line-range blocks, tables, and paragraphs.

=== Lists

In Zettelmarkup, lists themselves are not specified, but list items.
A sequence of list items is considered as a list.
>





|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007030000
title: Zettelmarkup: Blocks-Structured Elements
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Every markup for blocks-structured elements (""blocks"") begins at the very first position of a line.

There are five kinds of blocks: lists, one-line blocks, line-range blocks, tables, and paragraphs.

=== Lists

In Zettelmarkup, lists themselves are not specified, but list items.
A sequence of list items is considered as a list.
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
* [[Headings|00001007030300]] allow to structure the content of a zettel.
* The [[horizontal rule|00001007030400]] signals a thematic break

=== Line-range blocks

This kind of blocks encompass at least two lines.
To be useful, they encompass more lines.
They start with at least three identical characters at the first position of the beginning line.
They end at the line, that contains at least the same number of these identical characters, starting at the first position of that line.
This allows line-range blocks to be nested.
Additionally, all other blocks elements are allowed in line-range blocks.

* [[Verbatim blocks|00001007030500]] do not interpret their content,
* [[Quotation blocks|00001007030600]] specify a block-length quotation,
* [[Verse blocks|00001007030700]] allow to enter poetry, lyrics and similar text, where line endings are important
* [[Region blocks|00001007030800]] just mark a region, e.g. for common formatting
* [[Comment blocks|00001007030900]] allow to enter text that will be ignored when rendered

=== Tables

Similar to lists are tables not specified explicitly.
A sequence of table rows is considered a [[table|00001007031000]].
A table row itself is a sequence of table cells.

=== Paragraphs

Any line that does not conform to another blocks-structured element starts a paragraph.
This has the implication that a mistyped syntax element for a block element will be part of the paragraph. For example:
```zmk
= Heading
Some text follows.
```
will be rendered in HTML as
:::example
= Heading
Some text follows.
:::
This is because headings need at least three equal sign character.

A paragraph is essentially a sequence of [[inline-structured elements|00001007040000]].
Inline-structured elements cam span more than one line.
Paragraphs are separated by empty lines.

If you want to specify a second paragraph inside a list item, or if you want to continue a paragraph on a second and more line within a list item, you must start the paragraph with a certain number of space characters.
The number of space characters depends on the kind of a list and the relevant nesting level.

A line that starts with a space character and which is outside of a list or does not contain the right number of space characters is considered to be part of a paragraph.







|
|

















|
















|


|
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
* [[Headings|00001007030300]] allow to structure the content of a zettel.
* The [[horizontal rule|00001007030400]] signals a thematic break

=== Line-range blocks

This kind of blocks encompass at least two lines.
To be useful, they encompass more lines.
They begin with at least three identical characters at the first position of the beginning line.
They end at the line, that contains at least the same number of these identical characters, beginning at the first position of that line.
This allows line-range blocks to be nested.
Additionally, all other blocks elements are allowed in line-range blocks.

* [[Verbatim blocks|00001007030500]] do not interpret their content,
* [[Quotation blocks|00001007030600]] specify a block-length quotation,
* [[Verse blocks|00001007030700]] allow to enter poetry, lyrics and similar text, where line endings are important
* [[Region blocks|00001007030800]] just mark a region, e.g. for common formatting
* [[Comment blocks|00001007030900]] allow to enter text that will be ignored when rendered

=== Tables

Similar to lists are tables not specified explicitly.
A sequence of table rows is considered a [[table|00001007031000]].
A table row itself is a sequence of table cells.

=== Paragraphs

Any line that does not conform to another blocks-structured element begins a paragraph.
This has the implication that a mistyped syntax element for a block element will be part of the paragraph. For example:
```zmk
= Heading
Some text follows.
```
will be rendered in HTML as
:::example
= Heading
Some text follows.
:::
This is because headings need at least three equal sign character.

A paragraph is essentially a sequence of [[inline-structured elements|00001007040000]].
Inline-structured elements cam span more than one line.
Paragraphs are separated by empty lines.

If you want to specify a second paragraph inside a list item, or if you want to continue a paragraph on a second and more line within a list item, you must begin the paragraph with a certain number of space characters.
The number of space characters depends on the kind of a list and the relevant nesting level.

A line that begins with a space character and which is outside of a list or does not contain the right number of space characters is considered to be part of a paragraph.

Changes to docs/manual/00001007030100.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

title: Zettelmarkup: Description Lists
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

A description list is a sequence of terms to be described together with the descriptions of each term.
Every term can described in multiple ways.

A description term (short: //term//) is specified with one semicolon (""'';''"", ''U+003B'') at the first position, followed by a space character and the described term, specified as a sequence of line elements.
If the following lines should also be part of the term, exactly two spaces must be given at the start the each following line.

The description of a term is given with one colon (""'':''"", ''U+003A'') at the first position, followed by a space character and the description itself, specified as a sequence of inline elements.
Similar to terms, following lines can also be part of the actual description, if they start at each line with exactly two space characters.

In contrast to terms, the actual descriptions are merged into a paragraph.
This is because, an actual description can contain more than one paragraph.
As usual, paragraphs are separated by an empty line.
Every following paragraph of an actual description must be indented by two space characters.

Example:
>









|


|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
id: 00001007030100
title: Zettelmarkup: Description Lists
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

A description list is a sequence of terms to be described together with the descriptions of each term.
Every term can described in multiple ways.

A description term (short: //term//) is specified with one semicolon (""'';''"", ''U+003B'') at the first position, followed by a space character and the described term, specified as a sequence of line elements.
If the following lines should also be part of the term, exactly two spaces must be given at the beginning of each following line.

The description of a term is given with one colon (""'':''"", ''U+003A'') at the first position, followed by a space character and the description itself, specified as a sequence of inline elements.
Similar to terms, following lines can also be part of the actual description, if they begin at each line with exactly two space characters.

In contrast to terms, the actual descriptions are merged into a paragraph.
This is because, an actual description can contain more than one paragraph.
As usual, paragraphs are separated by an empty line.
Every following paragraph of an actual description must be indented by two space characters.

Example:

Changes to docs/manual/00001007030200.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14

15
16
17
18
19
20
21

title: Zettelmarkup: Nested Lists
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

There are thee kinds of lists that can be nested: ordered lists, unordered lists, and quotation lists.

Ordered lists are specified with the number sign (""''#''"", ''U+0023''), unordered lists use the asterisk (""''*''"", ''U+002A''), and quotation lists are specified with the greater-than sing (""''>''"", ''U+003E'').
Let's call these three characters //list characters//.

Any nested list item is specified by a non-empty sequence of list characters, followed by a space character and a sequence of inline elements.
In case of a quotation list as the last list character, the space character followed by a sequence of inline elements is optional.
The number / count of list characters gives the nesting of the lists.
If the following lines should also be part of the list item, exactly the same number of spaces must be given at the start the each following line as it is the lists are nested, plus one additional space character. In other words: the inline elements must start at the same column as it was on the previous line.


The resulting sequence on inline elements is merged into a paragraph.
Appropriately indented paragraphs can specified after the first one.
Since each blocks-structured element has to be specified at the first position of a line, none of the nested list items may contain anything else than paragraphs.

Some examples:
```zmk
>













|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
id: 00001007030200
title: Zettelmarkup: Nested Lists
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

There are thee kinds of lists that can be nested: ordered lists, unordered lists, and quotation lists.

Ordered lists are specified with the number sign (""''#''"", ''U+0023''), unordered lists use the asterisk (""''*''"", ''U+002A''), and quotation lists are specified with the greater-than sing (""''>''"", ''U+003E'').
Let's call these three characters //list characters//.

Any nested list item is specified by a non-empty sequence of list characters, followed by a space character and a sequence of inline elements.
In case of a quotation list as the last list character, the space character followed by a sequence of inline elements is optional.
The number / count of list characters gives the nesting of the lists.
If the following lines should also be part of the list item, exactly the same number of spaces must be given at the beginning of each of the following lines as it is the lists are nested, plus one additional space character.
In other words: the inline elements must begin at the same column as it was on the previous line.

The resulting sequence on inline elements is merged into a paragraph.
Appropriately indented paragraphs can specified after the first one.
Since each blocks-structured element has to be specified at the first position of a line, none of the nested list items may contain anything else than paragraphs.

Some examples:
```zmk

Changes to docs/manual/00001007030300.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14

title: Zettelmarkup: Headings
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

To specify a (sub-) section of a zettel, you should use the headings syntax: at
the start of a new line type at least three equal signs (""''=''"", ''U+003D''), plus at least one
space and enter the text of the heading as inline elements.

```zmk
=== Level 1 Heading
==== Level 2 Heading
===== Level 3 Heading
====== Level 4 Heading
>






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
id: 00001007030300
title: Zettelmarkup: Headings
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

To specify a (sub-) section of a zettel, you should use the headings syntax: at
the beginning of a new line type at least three equal signs (""''=''"", ''U+003D''), plus at least one
space and enter the text of the heading as inline elements.

```zmk
=== Level 1 Heading
==== Level 2 Heading
===== Level 3 Heading
====== Level 4 Heading

Changes to docs/manual/00001007030400.zettel.


1
2
3
4
5
6
7

title: Zettelmarkup: Horizontal Rule
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

To signal a thematic break, you can specify a horizonal rule.
This is done by entering at least three hyphen-minus characters (""''-''"", ''U+002D'') at the first position of a line.
>







1
2
3
4
5
6
7
8
id: 00001007030400
title: Zettelmarkup: Horizontal Rule
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

To signal a thematic break, you can specify a horizonal rule.
This is done by entering at least three hyphen-minus characters (""''-''"", ''U+002D'') at the first position of a line.

Changes to docs/manual/00001007030500.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

title: Zettelmarkup: Verbatim Blocks
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Verbatim blocks are used to enter text that should not be interpreted.
They start with at least three grave accent characters (""''`''"", ''U+0060'') at the first position of a line.
Alternatively, a modifier letter grave accent (""''Ë‹''"", ''U+02CB'') is also allowed[^On some devices, such as an iPhone / iPad, a grave accent character is harder to enter and is often confused with a modifier letter grave accent.].

You can add some [[attributes|00001007050000]] on the start line of a verbatim block, following the initiating characters.
The verbatim block supports the default attribute: when given, all spaces in the text are rendered in HTML as open box characters (""''&#x2423;''"", ''U+2423'').
If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value.
It will be interpreted as a (programming) language to support colourizing the text when rendered in HTML.

Any other character in this line will be ignored

Text following the starting line will not be interpreted, until a line starts with at least the same number of the same characters given at the starting line.
This allows to enter some grave accent characters in the text that should not be interpreted.

For example:
`````zmk
````zmk
```
````
>






|


|






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
id: 00001007030500
title: Zettelmarkup: Verbatim Blocks
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Verbatim blocks are used to enter text that should not be interpreted.
They begin with at least three grave accent characters (""''`''"", ''U+0060'') at the first position of a line.
Alternatively, a modifier letter grave accent (""''Ë‹''"", ''U+02CB'') is also allowed[^On some devices, such as an iPhone / iPad, a grave accent character is harder to enter and is often confused with a modifier letter grave accent.].

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.
The verbatim block supports the default attribute: when given, all spaces in the text are rendered in HTML as open box characters (""''&#x2423;''"", ''U+2423'').
If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value.
It will be interpreted as a (programming) language to support colourizing the text when rendered in HTML.

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some grave accent characters in the text that should not be interpreted.

For example:
`````zmk
````zmk
```
````

Changes to docs/manual/00001007030600.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

title: Zettelmarkup: Quotation Blocks
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

A simple way to enter a quotation is to use the [[quotation list|00001007030200]].
A quotation list loosely follows the convention of quoting text within emails.
However, if you want to attribute the quotation to seomeone, a quotation block is more appropriately.

This kind of line-range block starts with at least three less-than characters (""''<''"", ''U+003C'') at the first position of a line.
You can add some [[attributes|00001007050000]] on the start line of a quotation block, following the initiating characters.
The quotation does not support the default attribute, nor the generic attribute.
Attributes are interpreted on HTML rendering.
Any other character in this line will be ignored

Text following the starting line will be interpreted, until a line starts with at least the same number of the same characters given at the starting line.
This allows to enter a quotation block within a quotation block.
At the ending line, you can enter some [[inline elements|00001007040000]] after the less-than characters.
These will interpreted as some attribution text.

For example:

```zmk
>









|
|




|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
id: 00001007030600
title: Zettelmarkup: Quotation Blocks
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

A simple way to enter a quotation is to use the [[quotation list|00001007030200]].
A quotation list loosely follows the convention of quoting text within emails.
However, if you want to attribute the quotation to seomeone, a quotation block is more appropriately.

This kind of line-range block begins with at least three less-than characters (""''<''"", ''U+003C'') at the first position of a line.
You can add some [[attributes|00001007050000]] on the beginning line of a quotation block, following the initiating characters.
The quotation does not support the default attribute, nor the generic attribute.
Attributes are interpreted on HTML rendering.
Any other character in this line will be ignored

Text following the beginning line will be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter a quotation block within a quotation block.
At the ending line, you can enter some [[inline elements|00001007040000]] after the less-than characters.
These will interpreted as some attribution text.

For example:

```zmk

Changes to docs/manual/00001007030700.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

title: Zettelmarkup: Verse Blocks
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Sometimes, you want to enter text with significant space characters at the beginning of each line and with significant line endings.
Poetry is one typical example.
Of course, you could help yourself with hard space characters and hard line breaks, by entering a backslash character before a space character and at the end of each line.
Using a verse block might be easier.

This kind of line-range block starts with at least three quotation mark characters (""''"''"", ''U+0022'') at the first position of a line.
You can add some [[attributes|00001007050000]] on the start line of a verse block, following the initiating characters.
The verse block does not support the default attribute, nor the generic attribute.
Attributes are interpreted on HTML rendering.
Any other character in this line will be ignored.

Text following the starting line will be interpreted, until a line starts with at least the same number of the same characters given at the starting line.
This allows to enter a verse block within a verse block.
At the ending line, you can enter some [[inline elements|00001007040000]] after the quotation mark characters.
These will interpreted as some attribution text.

For example:

```zmk
>










|
|




|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
id: 00001007030700
title: Zettelmarkup: Verse Blocks
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Sometimes, you want to enter text with significant space characters at the beginning of each line and with significant line endings.
Poetry is one typical example.
Of course, you could help yourself with hard space characters and hard line breaks, by entering a backslash character before a space character and at the end of each line.
Using a verse block might be easier.

This kind of line-range block begins with at least three quotation mark characters (""''"''"", ''U+0022'') at the first position of a line.
You can add some [[attributes|00001007050000]] on the beginning line of a verse block, following the initiating characters.
The verse block does not support the default attribute, nor the generic attribute.
Attributes are interpreted on HTML rendering.
Any other character in this line will be ignored.

Text following the beginning line will be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter a verse block within a verse block.
At the ending line, you can enter some [[inline elements|00001007040000]] after the quotation mark characters.
These will interpreted as some attribution text.

For example:

```zmk

Changes to docs/manual/00001007030800.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

title: Zettelmarkup: Region Blocks
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Region blocks does not directly have a visual representation.
They just group a range of lines.
You can use region blocks to enter [[attributes|00001007050000]] that apply only to this range of lines.
One example is to enter a multi-line warning that should be visible.

This kind of line-range block starts with at least three colon characters (""'':''"", ''U+003A'') at the first position of a line[^Since a [[description text|00001007030100]] only use exactly one colon character at the first position of a line, there is no possible ambiguity between these elements.].
You can add some [[attributes|00001007050000]] on the start line of a verse block, following the initiating characters.
The region block does not support the default attribute, but it supports the generic attribute.
Some generic attributes, like ``=note``, ``=warning`` will be rendered special.
Attributes are interpreted on HTML rendering.
Any other character in this line will be ignored.

Text following the starting line will be interpreted, until a line starts with at least the same number of the same characters given at the starting line.
This allows to enter a region block within a region block.
At the ending line, you can enter some [[inline elements|00001007040000]] after the colon characters.
These will interpreted as some attribution text.

For example:

```zmk
>










|
|





|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
id: 00001007030800
title: Zettelmarkup: Region Blocks
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Region blocks does not directly have a visual representation.
They just group a range of lines.
You can use region blocks to enter [[attributes|00001007050000]] that apply only to this range of lines.
One example is to enter a multi-line warning that should be visible.

This kind of line-range block begins with at least three colon characters (""'':''"", ''U+003A'') at the first position of a line[^Since a [[description text|00001007030100]] only use exactly one colon character at the first position of a line, there is no possible ambiguity between these elements.].
You can add some [[attributes|00001007050000]] on the beginning line of a verse block, following the initiating characters.
The region block does not support the default attribute, but it supports the generic attribute.
Some generic attributes, like ``=note``, ``=warning`` will be rendered special.
Attributes are interpreted on HTML rendering.
Any other character in this line will be ignored.

Text following the beginning line will be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter a region block within a region block.
At the ending line, you can enter some [[inline elements|00001007040000]] after the colon characters.
These will interpreted as some attribution text.

For example:

```zmk

Changes to docs/manual/00001007030900.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

title: Zettelmarkup: Comment Blocks
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Comment blocks are quite similar to [[verbatim blocks|00001007030500]]: both are used to enter text that should not be interpreted.
While the text entered inside a verbatim block will be processed somehow, text inside a comment block will be ignored[^Well, not completely ignored: text is read, but it will typically not rendered visible.].
Comment blocks are typically used to give some internal comments, e.g. the license of a text or some internal remarks.

Comment blocks start with at least three percent sign characters (""''%''"", ''U+0025'') at the first position of a line.
You can add some [[attributes|00001007050000]] on the start line of a comment block, following the initiating characters.
The comment block supports the default attribute: when given, the text will be rendered, e.g. as an HTML comment.
When rendered to JSON, the comment block will not be ignored but it will output some JSON text.
Same for other renderers.

Any other character in this line will be ignored

Text following the starting line will not be interpreted, until a line starts with at least the same number of the same characters given at the starting line.
This allows to enter some percent sign characters in the text that should not be interpreted.

For example:
```zmk
%%%
Comment
  Block
>









|
|






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
id: 00001007030900
title: Zettelmarkup: Comment Blocks
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Comment blocks are quite similar to [[verbatim blocks|00001007030500]]: both are used to enter text that should not be interpreted.
While the text entered inside a verbatim block will be processed somehow, text inside a comment block will be ignored[^Well, not completely ignored: text is read, but it will typically not rendered visible.].
Comment blocks are typically used to give some internal comments, e.g. the license of a text or some internal remarks.

Comment blocks begin with at least three percent sign characters (""''%''"", ''U+0025'') at the first position of a line.
You can add some [[attributes|00001007050000]] on the beginning line of a comment block, following the initiating characters.
The comment block supports the default attribute: when given, the text will be rendered, e.g. as an HTML comment.
When rendered to JSON, the comment block will not be ignored but it will output some JSON text.
Same for other renderers.

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some percent sign characters in the text that should not be interpreted.

For example:
```zmk
%%%
Comment
  Block

Changes to docs/manual/00001007031000.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

title: Zettelmarkup: Tables
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Tables are used to show some data in a two-dimenensional fashion.
In zettelmarkup, table are not specified explicitly, but by entering //table rows//.
Therefore, a table can be seen as a sequence of table rows.
A table row is nothing as a sequence of //table cells//.
The length of a table is the number of table rows, the width of a table is the maximum length of its rows.

The first cell of a row must start with the vertical bar character (""''|''"", ''U+007C'') at the first position of a line.
The other cells of a row start with the same vertical bar character at later positions in that line.
A cell is delimited by the vertical bar character of the next cell or by the end of the current line.
A vertical bar character as the last character of a line will not result in a table cell.
It will be ignored.
Inside a cell, you can specify any [[inline elements|00001007040000]].

For example:
```zmk
>











|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
id: 00001007031000
title: Zettelmarkup: Tables
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Tables are used to show some data in a two-dimenensional fashion.
In zettelmarkup, table are not specified explicitly, but by entering //table rows//.
Therefore, a table can be seen as a sequence of table rows.
A table row is nothing as a sequence of //table cells//.
The length of a table is the number of table rows, the width of a table is the maximum length of its rows.

The first cell of a row must begin with the vertical bar character (""''|''"", ''U+007C'') at the first position of a line.
The other cells of a row begin with the same vertical bar character at later positions in that line.
A cell is delimited by the vertical bar character of the next cell or by the end of the current line.
A vertical bar character as the last character of a line will not result in a table cell.
It will be ignored.
Inside a cell, you can specify any [[inline elements|00001007040000]].

For example:
```zmk

Changes to docs/manual/00001007040000.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

title: Zettelmarkup: Inline-Structured Elements
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Most characters you type is concerned with inline-structured elements.
The content of a zettel contains is many cases just ordinary text, lightly formatted.
Inline-structured elements allow to format your text and add some helpful links or images.
Sometimes, you want to enter characters that have no representation on your keyboard.

=== Text formatting
Every [[text formatting|00001007040100]] element starts with two same characters at the beginning.
It lasts until the same two characters occurred the second time.
Some of these elements explicitly support [[attributes|00001007050000]].

=== Literal-like formatting
Sometime you want to render the text as it is.
This is the core motivation of [[literal-like formatting|00001007040200]].

=== Reference-like text
You can reference other zettel and (external) material within one zettel.
This kind of reference may be a link, or an images that is display inline when the zettel is rendered.
Footnotes sometimes factor out some useful text that hinders the flow of reading text.
Internal marks allow to reference something within a zettel.
An important aspect of all knowledge work is to reference others work, e.g. with citation keys.
All these elements can be subsumed under [[reference-like text|00001007040300]].

=== Other inline elements
==== Comments
A comment is started with two consecutive percent sign characters (""''%''"", ''U+0025'').
It ends at the end of the line where it started.

==== Backslash
The backslash character (""''\\''"", ''U+005C'') gives the next character another meaning.
* If a space character follows, it is converted in a non-breaking space (''U+00A0'').
* If a line ending follows the backslash character, the line break is converted from a //soft break// into a //hard break//.
* Every other character is taken as itself, but without the interpretation of a Zettelmarkup element.
  For example, if you want to enter a ""'']''"" into a footnote text, you should escape it with a backslash.

==== Tag
Any text that starts with a number sign character (""''#''"", ''U+0023''), followed by a non-empty sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low line character (""''_''"", ''U+005F'') is interpreted as an //inline tag//.
They will be considered equivalent to tags in metadata.

==== Entities & more
Sometimes it is not easy to enter special characters.
If you know the Unicode code point of that character, or its name according to the [[HTML standard|https://html.spec.whatwg.org/multipage/named-characters.html]], you can enter it by number or by name.

Regardless which method you use, an entity always starts with an ampersand character (""''&''"", ''U+0026'') and ends with a semicolon character (""'';''"", ''U+003B'').
If you know the HTML name of the character you want to enter, place it between these two character.
Example: ``&amp;`` is rendered as ::&amp;::{=example}.

If you want to enter its numeric code point, a number sign character must follow the ampersand character, followed by digits to base 10.
Example: ``&#38;`` is rendered in HTML as ::&#38;::{=example}.

You also can enter its numeric code point as a hex number, if you place the letter ""x"" after the numeric sign character.
>











|

















|
|









|






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
id: 00001007040000
title: Zettelmarkup: Inline-Structured Elements
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Most characters you type is concerned with inline-structured elements.
The content of a zettel contains is many cases just ordinary text, lightly formatted.
Inline-structured elements allow to format your text and add some helpful links or images.
Sometimes, you want to enter characters that have no representation on your keyboard.

=== Text formatting
Every [[text formatting|00001007040100]] element begins with two same characters at the beginning.
It lasts until the same two characters occurred the second time.
Some of these elements explicitly support [[attributes|00001007050000]].

=== Literal-like formatting
Sometime you want to render the text as it is.
This is the core motivation of [[literal-like formatting|00001007040200]].

=== Reference-like text
You can reference other zettel and (external) material within one zettel.
This kind of reference may be a link, or an images that is display inline when the zettel is rendered.
Footnotes sometimes factor out some useful text that hinders the flow of reading text.
Internal marks allow to reference something within a zettel.
An important aspect of all knowledge work is to reference others work, e.g. with citation keys.
All these elements can be subsumed under [[reference-like text|00001007040300]].

=== Other inline elements
==== Comments
A comment begins with two consecutive percent sign characters (""''%''"", ''U+0025'').
It ends at the end of the line where it begins.

==== Backslash
The backslash character (""''\\''"", ''U+005C'') gives the next character another meaning.
* If a space character follows, it is converted in a non-breaking space (''U+00A0'').
* If a line ending follows the backslash character, the line break is converted from a //soft break// into a //hard break//.
* Every other character is taken as itself, but without the interpretation of a Zettelmarkup element.
  For example, if you want to enter a ""'']''"" into a footnote text, you should escape it with a backslash.

==== Tag
Any text that begins with a number sign character (""''#''"", ''U+0023''), followed by a non-empty sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low line character (""''_''"", ''U+005F'') is interpreted as an //inline tag//.
They will be considered equivalent to tags in metadata.

==== Entities & more
Sometimes it is not easy to enter special characters.
If you know the Unicode code point of that character, or its name according to the [[HTML standard|https://html.spec.whatwg.org/multipage/named-characters.html]], you can enter it by number or by name.

Regardless which method you use, an entity always begins with an ampersand character (""''&''"", ''U+0026'') and ends with a semicolon character (""'';''"", ''U+003B'').
If you know the HTML name of the character you want to enter, place it between these two character.
Example: ``&amp;`` is rendered as ::&amp;::{=example}.

If you want to enter its numeric code point, a number sign character must follow the ampersand character, followed by digits to base 10.
Example: ``&#38;`` is rendered in HTML as ::&#38;::{=example}.

You also can enter its numeric code point as a hex number, if you place the letter ""x"" after the numeric sign character.

Changes to docs/manual/00001007040100.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

title: Zettelmarkup: Text Formatting
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Text formatting is the way to make your text visually different.
Every text formatting element starts with two same characters.
It ends when these two same characters occur the second time.
It is possible that some [[attributes|00001007050000]] follow immediately, without any separating character.

Text formatting can be nested, up to a reasonable limit.

The following characters start a text formatting:

* The slash character (""''/''"", ''U+002F'') emphasizes its text. Often, such text is rendered in italics. If the default attribute is specified, the emphasized text is not just rendered as such, but also internally marked as emphasized.
** Example: ``abc //def// ghi`` is rendered in HTML as: ::abc //def// ghi::{=example}.
** Example: ``abc //def//{-} ghi`` is rendered in HTML as: ::abc //def//{-} ghi::{=example}.
* The asterisk character (""''*''"", ''U+002A'') strongly emphasized its enclosed text. The text is often rendered in bold. Again, the default attribute will force a explicit semantic meaning of strong emphasizing.
** Example: ``abc **def** ghi`` is rendered in HTML as: ::abc **def** ghi::{=example}.
** Example: ``abc **def**{-} ghi`` is rendered in HTML as: ::abc **def**{-} ghi::{=example}.
>






|





|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
id: 00001007040100
title: Zettelmarkup: Text Formatting
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Text formatting is the way to make your text visually different.
Every text formatting element begins with two same characters.
It ends when these two same characters occur the second time.
It is possible that some [[attributes|00001007050000]] follow immediately, without any separating character.

Text formatting can be nested, up to a reasonable limit.

The following characters begin a text formatting:

* The slash character (""''/''"", ''U+002F'') emphasizes its text. Often, such text is rendered in italics. If the default attribute is specified, the emphasized text is not just rendered as such, but also internally marked as emphasized.
** Example: ``abc //def// ghi`` is rendered in HTML as: ::abc //def// ghi::{=example}.
** Example: ``abc //def//{-} ghi`` is rendered in HTML as: ::abc //def//{-} ghi::{=example}.
* The asterisk character (""''*''"", ''U+002A'') strongly emphasized its enclosed text. The text is often rendered in bold. Again, the default attribute will force a explicit semantic meaning of strong emphasizing.
** Example: ``abc **def** ghi`` is rendered in HTML as: ::abc **def** ghi::{=example}.
** Example: ``abc **def**{-} ghi`` is rendered in HTML as: ::abc **def**{-} ghi::{=example}.

Changes to docs/manual/00001007040200.zettel.


1
2
3
4
5
6
7

title: Zettelmarkup: Literal-like formatting
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

There are some reasons to mark text that should be rendered as uninterpreted:
* Mark text as literal, sometimes as part of a program.
>







1
2
3
4
5
6
7
8
id: 00001007040200
title: Zettelmarkup: Literal-like formatting
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

There are some reasons to mark text that should be rendered as uninterpreted:
* Mark text as literal, sometimes as part of a program.

Changes to docs/manual/00001007040300.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

title: Zettelmarkup: Reference-like text
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

An important aspect of knowledge work is to interconnect your zettel as well as provide links to (external) material.

There are several kinds of references that are allowed in Zettelmarkup:
* Links to other zettel.
* Links to (external material).
* Embed images that are stored within your Zettelstore.
* Embed external images.
* Reference via a footnote.
* Reference via a citation key.
* Put a mark within your zettel that you can reference later with a link.

=== Links
There are two kinds of links, regardless of links to (internal) other zettel or to (external) material.
Both kinds starts with two consecutive left square bracket characters (""''[''"", ''U+005B'') and ends with two consecutive right square bracket characters (""'']''"", ''U+005D'').

The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", ''U+007C''): ``[[text|linkspecification]]``.

The second form just provides a link specification between the square brackets.
Its text is derived from the link specification, e.g. by interpreting the link specification as text: ``[[linkspecification]]``.

The link specification for another zettel within the same Zettelstore is just the [[zettel identifier|00001006050000]].
To reference some content within a zettel, you can append a number sign character (""''#''"", ''U+0023'') and the name of the mark to the zettel identifier.
The resulting reference is called ""zettel reference"".

To specify some material outside the Zettelstore, just use an normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://tools.ietf.org/html/rfc3986]].
If the URL starts with the slash character (""/"", ''U+002F''), i.e. without scheme, user info, and host name, or if it starts with ""./"" or with ""../"", the reference will be treated as a ""local reference"", otherwise as an ""external reference"".


The text in the second form is just a sequence of inline elements.

=== Images
To some degree, an image specification is conceptually not too far away from a link specification.
Both contain a link specification and optionally some text.
In contrast to a link, the link specification of an image must resolve to actual graphical image data.
That data is read when rendered as HTML, and is embedded inside the zettel as an inline image.

An image specification starts with two consecutive left curly bracket characters (""''{''"", ''U+007B'') and ends with two consecutive right curly bracket characters (""''}''"", ''U+007D'').
The curly brackets delimits either a link specification or some text, a vertical bar character and the link specification, similar to a link.

One difference to a link: if the text was not given, an empty string is assumed.

The link specification must reference a graphical image representation if the image is about to be rendered.
Supported formats are:

>


















|











|
>









|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
id: 00001007040300
title: Zettelmarkup: Reference-like text
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

An important aspect of knowledge work is to interconnect your zettel as well as provide links to (external) material.

There are several kinds of references that are allowed in Zettelmarkup:
* Links to other zettel.
* Links to (external material).
* Embed images that are stored within your Zettelstore.
* Embed external images.
* Reference via a footnote.
* Reference via a citation key.
* Put a mark within your zettel that you can reference later with a link.

=== Links
There are two kinds of links, regardless of links to (internal) other zettel or to (external) material.
Both kinds begin with two consecutive left square bracket characters (""''[''"", ''U+005B'') and ends with two consecutive right square bracket characters (""'']''"", ''U+005D'').

The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", ''U+007C''): ``[[text|linkspecification]]``.

The second form just provides a link specification between the square brackets.
Its text is derived from the link specification, e.g. by interpreting the link specification as text: ``[[linkspecification]]``.

The link specification for another zettel within the same Zettelstore is just the [[zettel identifier|00001006050000]].
To reference some content within a zettel, you can append a number sign character (""''#''"", ''U+0023'') and the name of the mark to the zettel identifier.
The resulting reference is called ""zettel reference"".

To specify some material outside the Zettelstore, just use an normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://tools.ietf.org/html/rfc3986]].
If the URL begins with the slash character (""/"", ''U+002F''), or if it begins with ""./"" or with ""../"", i.e. without scheme, user info, and host name, the reference will be treated as a ""local reference"", otherwise as an ""external reference"".
If the URL begins with two slash characters, it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]].

The text in the second form is just a sequence of inline elements.

=== Images
To some degree, an image specification is conceptually not too far away from a link specification.
Both contain a link specification and optionally some text.
In contrast to a link, the link specification of an image must resolve to actual graphical image data.
That data is read when rendered as HTML, and is embedded inside the zettel as an inline image.

An image specification begins with two consecutive left curly bracket characters (""''{''"", ''U+007B'') and ends with two consecutive right curly bracket characters (""''}''"", ''U+007D'').
The curly brackets delimits either a link specification or some text, a vertical bar character and the link specification, similar to a link.

One difference to a link: if the text was not given, an empty string is assumed.

The link specification must reference a graphical image representation if the image is about to be rendered.
Supported formats are:

59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

%%Example:

%%``{{External link|00000000030001}}{title=External width=30}`` is rendered as ::{{External link|00000000030001}}{title=External width=30}::{=example}.

=== Footnotes

A footnote starts with a left square bracket, followed by a circumflex accent (""''^''"", ''U+005E''), followed by some text, and ends with a right square bracket.

Example:

``Main text[^Footnote text.].`` is rendered in HTML as: ::Main text[^Footnote text.].::{=example}.

=== Citation key

A citation key references some external material that is part of a bibliografical collection.

Currently, Zettelstore implements this only partially, it is ""work in progress"".

However, the syntax is: starting with a left square bracket and followed by an at sign character (""''@''"", ''U+0040''), a the citation key is given.
The key is typically a sequence of letters and digits.
If a comma character (""'',''"", ''U+002C'') or a vertical bar character is given, the following is interpreted as inline elements.
A right square bracket ends the text and the citation key element.

=== Mark
A mark allows to name a point within a zettel.
This is useful if you want to reference some content in a bigger-sized zettel[^Other uses of marks will be given, if Zettelmarkup is extended by a concept called //transclusion//.].

A mark starts with a left square bracket, followed by an exclamation mark character (""''!''"", ''U+0021'').
Now the optional mark name follows.
It is a (possibly empty) sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low-line character (""''_''"", ''U+005F'').
The mark element ends with a right square bracket.

Examples:
* ``[!]`` is a mark without a name, the empty mark.
* ``[!mark]`` is a mark with the name ""mark"".







|











|








|







61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

%%Example:

%%``{{External link|00000000030001}}{title=External width=30}`` is rendered as ::{{External link|00000000030001}}{title=External width=30}::{=example}.

=== Footnotes

A footnote begins with a left square bracket, followed by a circumflex accent (""''^''"", ''U+005E''), followed by some text, and ends with a right square bracket.

Example:

``Main text[^Footnote text.].`` is rendered in HTML as: ::Main text[^Footnote text.].::{=example}.

=== Citation key

A citation key references some external material that is part of a bibliografical collection.

Currently, Zettelstore implements this only partially, it is ""work in progress"".

However, the syntax is: beginning with a left square bracket and followed by an at sign character (""''@''"", ''U+0040''), a the citation key is given.
The key is typically a sequence of letters and digits.
If a comma character (""'',''"", ''U+002C'') or a vertical bar character is given, the following is interpreted as inline elements.
A right square bracket ends the text and the citation key element.

=== Mark
A mark allows to name a point within a zettel.
This is useful if you want to reference some content in a bigger-sized zettel[^Other uses of marks will be given, if Zettelmarkup is extended by a concept called //transclusion//.].

A mark begins with a left square bracket, followed by an exclamation mark character (""''!''"", ''U+0021'').
Now the optional mark name follows.
It is a (possibly empty) sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low-line character (""''_''"", ''U+005F'').
The mark element ends with a right square bracket.

Examples:
* ``[!]`` is a mark without a name, the empty mark.
* ``[!mark]`` is a mark with the name ""mark"".

Changes to docs/manual/00001007050000.zettel.


1
2
3
4
5
6
7

title: Zettelmarkup: Attributes
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Attributes allows to modify the way how material is presented.
Alternatively, they provide additional information to markup elements.
>







1
2
3
4
5
6
7
8
id: 00001007050000
title: Zettelmarkup: Attributes
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
role: manual

Attributes allows to modify the way how material is presented.
Alternatively, they provide additional information to markup elements.

Changes to docs/manual/00001007050100.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

title: Zettelmarkup: Supported Attribute Values for Natural Languages
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
role: manual

With an [[attribute|00001007050000]] it is possible to specify the natural language of a text region.
This is important, if you want to render your markup into an environment, where this is significant.
HTML is such an environment.

To specify the language within an attribute, you must use the key ''lang''.
The language itself is specified according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]].

Examples:
* ``{lang=en}`` for the english language
* ``{lang=en-us}`` for the english dialect spoken in the United States of America
* ``{lang=de}`` for the german language
* ``{lang=de-at}`` for the german language dialect spoken in Austria
* ``{lang=de-de}`` for the german language dialect spoken in Germany

The actual [[typographic quotations marks|00001007040100]] (``""...""``) are derived from the current language.
The language of a zettel (meta key ''lang'') or of the whole Zettelstore (''default-lang'' of the [[configuration zettel|00001004020000]]) can be overwritten by an attribute: ``""...""{lang=fr}``{=zmk}.
Currently, Zettelstore supports the following primary languages:

* ''de''
* ''en''
* ''fr''

These are used, even if a dialect was specified.
>




















|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
id: 00001007050100
title: Zettelmarkup: Supported Attribute Values for Natural Languages
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
role: manual

With an [[attribute|00001007050000]] it is possible to specify the natural language of a text region.
This is important, if you want to render your markup into an environment, where this is significant.
HTML is such an environment.

To specify the language within an attribute, you must use the key ''lang''.
The language itself is specified according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]].

Examples:
* ``{lang=en}`` for the english language
* ``{lang=en-us}`` for the english dialect spoken in the United States of America
* ``{lang=de}`` for the german language
* ``{lang=de-at}`` for the german language dialect spoken in Austria
* ``{lang=de-de}`` for the german language dialect spoken in Germany

The actual [[typographic quotations marks|00001007040100]] (``""...""``) are derived from the current language.
The language of a zettel (meta key ''lang'') or of the whole Zettelstore (''default-lang'' of the [[configuration zettel|00001004020000#default-lang]]) can be overwritten by an attribute: ``""...""{lang=fr}``{=zmk}.
Currently, Zettelstore supports the following primary languages:

* ''de''
* ''en''
* ''fr''

These are used, even if a dialect was specified.

Changes to docs/manual/00001007050200.zettel.


1
2
3
4
5
6

title: Zettelmarkup: Supported Attribute Values for Programming Languages
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
role: manual

TBD
>






1
2
3
4
5
6
7
id: 00001007050200
title: Zettelmarkup: Supported Attribute Values for Programming Languages
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
role: manual

TBD

Changes to docs/manual/00001007060000.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13

title: Zettelmarkup: Summary of Formatting Characters
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
role: manual

The following table gives an overview about the use of all characters that start a markup element.

|= Character :|= Blocks <|= Inlines <
| ''!''  | (free) | (free)
| ''"''  | [[Verse block|00001007030700]] | [[Typographic quotation mark|00001007040100]]
| ''#''  | [[Ordered list|00001007030200]] | [[Tag|00001007040000]]
| ''$''  | (reserved) | (reserved)
| ''%''  | [[Comment block|00001007030900]] | [[Comment|00001007040000]]
>





|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007060000
title: Zettelmarkup: Summary of Formatting Characters
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
role: manual

The following table gives an overview about the use of all characters that begin a markup element.

|= Character :|= Blocks <|= Inlines <
| ''!''  | (free) | (free)
| ''"''  | [[Verse block|00001007030700]] | [[Typographic quotation mark|00001007040100]]
| ''#''  | [[Ordered list|00001007030200]] | [[Tag|00001007040000]]
| ''$''  | (reserved) | (reserved)
| ''%''  | [[Comment block|00001007030900]] | [[Comment|00001007040000]]

Changes to docs/manual/00001008000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
id: 00001008000000
title: Other Markup Languages
role: manual
tags: #manual #zettelstore
syntax: zmk
modified: 20210111182215

[[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content.
Zettelstore is quite agnostic with respect to markup languages.
Of course, Zettelmarkup plays an important role.
However, with the exception of zettel titles, you can use any (markup) language that is supported:

* Markdown
* Images: GIF, PNG, JPEG, SVG
* CSS
* HTML template data
* Plain text, not further interpreted

The [[metadata key|00001006020000#syntax]] ""''syntax''"" specifies which language should be used.
If it is not given, the key ""''default-syntax''"" will be used (specified in the [[configuration zettel|00001004020000]]).
The following syntax values are supported:

; [!css]''css''
: A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML.
; [!gif]''gif''; [!jpeg]''jpeg''; [!jpg]''jpg''; [!png]''png''
: The formats for pixel graphics.
  Typically the data is stored in a separate file and the syntax is given in the ''.meta'' file.





<













|







1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
id: 00001008000000
title: Other Markup Languages
role: manual
tags: #manual #zettelstore
syntax: zmk


[[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content.
Zettelstore is quite agnostic with respect to markup languages.
Of course, Zettelmarkup plays an important role.
However, with the exception of zettel titles, you can use any (markup) language that is supported:

* Markdown
* Images: GIF, PNG, JPEG, SVG
* CSS
* HTML template data
* Plain text, not further interpreted

The [[metadata key|00001006020000#syntax]] ""''syntax''"" specifies which language should be used.
If it is not given, the key ""''default-syntax''"" will be used (specified in the [[configuration zettel|00001004020000#default-syntax]]).
The following syntax values are supported:

; [!css]''css''
: A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML.
; [!gif]''gif''; [!jpeg]''jpeg''; [!jpg]''jpg''; [!png]''png''
: The formats for pixel graphics.
  Typically the data is stored in a separate file and the syntax is given in the ''.meta'' file.

Changes to docs/manual/00001008010000.zettel.


1
2
3
4
5
6
7

title: Use Markdown as the main markup language of Zettelstore
tags: #manual #markdown #zettelstore
syntax: zmk
role: manual

If you are customized to use Markdown as your markup language, you can configure Zettelstore to support your decision.

>







1
2
3
4
5
6
7
8
id: 00001008010000
title: Use Markdown as the main markup language of Zettelstore
tags: #manual #markdown #zettelstore
syntax: zmk
role: manual

If you are customized to use Markdown as your markup language, you can configure Zettelstore to support your decision.

Changes to docs/manual/00001010000000.zettel.


1
2
3
4
5
6
7

title: Security
tags: #configuration #manual #security #zettelstore
syntax: zmk
role: manual

Your zettel could contain sensitive content.
You probably want to ensure that only authorized person can read and/or modify them.
>







1
2
3
4
5
6
7
8
id: 00001010000000
title: Security
tags: #configuration #manual #security #zettelstore
syntax: zmk
role: manual

Your zettel could contain sensitive content.
You probably want to ensure that only authorized person can read and/or modify them.

Changes to docs/manual/00001010040100.zettel.


1
2
3
4
5
6
7
8

title: Enable authentication
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
role: manual

To enable authentication, you must create a zettel that stores [[authentication data|00001010040200]] for the owner.
Then you must reference this zettel within the [[start-up configuration|00001004010000]] under the key ''owner''.
Once the start-up configuration contains a valid [[zettel identifier|00001006050000]] under that key, authentication is enabled.
>






|

1
2
3
4
5
6
7
8
9
id: 00001010040100
title: Enable authentication
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
role: manual

To enable authentication, you must create a zettel that stores [[authentication data|00001010040200]] for the owner.
Then you must reference this zettel within the [[start-up configuration|00001004010000#owner]] under the key ''owner''.
Once the start-up configuration contains a valid [[zettel identifier|00001006050000]] under that key, authentication is enabled.

Changes to docs/manual/00001010040200.zettel.


1
2
3
4
5
6
7

title: Creating an user zettel
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
role: manual

All data to be used for authenticating a user is store in a special zettel called ""user zettel"". 
A user zettel must have set the following three metadata fields:
>







1
2
3
4
5
6
7
8
id: 00001010040200
title: Creating an user zettel
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
role: manual

All data to be used for authenticating a user is store in a special zettel called ""user zettel"". 
A user zettel must have set the following three metadata fields:

Changes to docs/manual/00001010040400.zettel.


1
2
3
4
5
6
7

title: Authentication process
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
role: manual

When someone tries to authenticate itself with an user identifier / ""user name"" and a password, the following process is executed:

>







1
2
3
4
5
6
7
8
id: 00001010040400
title: Authentication process
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
role: manual

When someone tries to authenticate itself with an user identifier / ""user name"" and a password, the following process is executed:

Changes to docs/manual/00001010040700.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

title: Access token
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
role: manual

If an user is authenticated, an ""access token"" is created that must be sent with every request to prove the identity of the caller.
Otherwise the user will not be recognized by Zettelstore.

If the user was authenticated via the web interface, the access token is stored in a [[""session cookie""|https://en.wikipedia.org/wiki/HTTP_cookie#Session_cookie]].
When the web browser is closed, theses cookies are not saved.
If you want web browser to store the cookie as long as lifetime of that token, the owner must set ''persistent-cookie'' of the [[startup configuration|00001004010000]] to ''true''.

If the web browser remains inactive for a period, the user will be automatically logged off, because each access token has a limited lifetime.
The maximum length of this period is specified by the ''token-lifetime-html'' value of the startup configuration.
Every time a web page is displayed, a fresh token is created and stored inside the cookie.

If the user was authenticated via the API, the access token will be returned as the content of the response.
Typically, the lifetime of this token is more short term, e.g. 10 minutes.
It is specified by the ''token-timeout-api'' value of the startup configuration.
If you need more time, you can either [[re-authenticate|00001012050200]] the user or use an API call to [[renew the access token|00001012050400]].

If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), your must set the ''insecure-cookie'' value of the startup configuration to ''true''.
In most cases, such a scenario is not recommended, because user name and password will be transferred as plain text.
You could make use of such scenario if you know all parties that access the local network where you access the Zettelstore.
>










|


|




|


|


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
id: 00001010040700
title: Access token
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
role: manual

If an user is authenticated, an ""access token"" is created that must be sent with every request to prove the identity of the caller.
Otherwise the user will not be recognized by Zettelstore.

If the user was authenticated via the web interface, the access token is stored in a [[""session cookie""|https://en.wikipedia.org/wiki/HTTP_cookie#Session_cookie]].
When the web browser is closed, theses cookies are not saved.
If you want web browser to store the cookie as long as lifetime of that token, the owner must set ''persistent-cookie'' of the [[start-up configuration|00001004010000]] to ''true''.

If the web browser remains inactive for a period, the user will be automatically logged off, because each access token has a limited lifetime.
The maximum length of this period is specified by the ''token-lifetime-html'' value of the start-up configuration.
Every time a web page is displayed, a fresh token is created and stored inside the cookie.

If the user was authenticated via the API, the access token will be returned as the content of the response.
Typically, the lifetime of this token is more short term, e.g. 10 minutes.
It is specified by the ''token-timeout-api'' value of the start-up configuration.
If you need more time, you can either [[re-authenticate|00001012050200]] the user or use an API call to [[renew the access token|00001012050400]].

If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), your must set the ''insecure-cookie'' value of the start-up configuration to ''true''.
In most cases, such a scenario is not recommended, because user name and password will be transferred as plain text.
You could make use of such scenario if you know all parties that access the local network where you access the Zettelstore.

Changes to docs/manual/00001010070200.zettel.


1
2
3
4
5
6
7
8
9
10
11
12

title: Visibility rules for zettel
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
modified: 20201221174224

For every zettel you can specify under which condition the zettel is visible to others.
This is controlled with the metadata key [[''visibility''|00001006020000#visibility]].
The following values are supported:

; [!public]""public""
: The zettel is visible to everybody, even if the user is not authenticated.
>




<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001010070200
title: Visibility rules for zettel
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk


For every zettel you can specify under which condition the zettel is visible to others.
This is controlled with the metadata key [[''visibility''|00001006020000#visibility]].
The following values are supported:

; [!public]""public""
: The zettel is visible to everybody, even if the user is not authenticated.
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  This is for zettel with sensitive content, e.g. the [[configuration zettel|00001004020000]] or the various zettel that contains the templates for rendering zettel in HTML.
; [!expert]""expert""
: Only the owner of the Zettelstore can access the zettel, if runtime configuration [[''expert-mode''|00001004020000#expert-mode]] is set to a boolean true value.

  This is for zettel with sensitive content that might irritate the owner.
  Computed zettel with internal runtime information are examples for such a zettel.
; [!simple-expert]""simple-expert""
: The owner of the Zettelstore cab access the zettel, if expert mode is enabled, or if authentication is disabled and the Zettelstore is started without any command.

  The reason for this is to show all computed zettel to an user that started the Zettestore in simple mode.
  Many computed zettel should be given in error reporting and a new user might not be able to enable expert mode.

When you install a Zettelstore, only two zettel have visibility ""public"".
The first zettel is the zettel that contains CSS for displaying the web interface.
This is to ensure that the web interface looks nice even for not authenticated users.
The other zettel is the zettel containing the [[version|00000000000001]] of the Zettelstore.

Please note: if authentication is not enabled, every user has the same rights as the owner of a Zettelstore.
This is also true, if the Zettelstore runs additionally in [[read-only mode|00001004010000#read-only-mode]].
In this case, the [[runtime configuration zettel|00001004020000]] is shown (its visibility is ""owner"").
The [[startup configuration|00001004010000]] is not shown, because the associated computed zettel with identifier ''00000000000099'' is stored with the visibility ""expert"".
If you want to show such a zettel, you must set ''expert-mode'' to true.







|












|

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  This is for zettel with sensitive content, e.g. the [[configuration zettel|00001004020000]] or the various zettel that contains the templates for rendering zettel in HTML.
; [!expert]""expert""
: Only the owner of the Zettelstore can access the zettel, if runtime configuration [[''expert-mode''|00001004020000#expert-mode]] is set to a boolean true value.

  This is for zettel with sensitive content that might irritate the owner.
  Computed zettel with internal runtime information are examples for such a zettel.
; [!simple-expert]""simple-expert""
: The owner of the Zettelstore can access the zettel, if expert mode is enabled, or if authentication is disabled and the Zettelstore is started without any command.

  The reason for this is to show all computed zettel to an user that started the Zettestore in simple mode.
  Many computed zettel should be given in error reporting and a new user might not be able to enable expert mode.

When you install a Zettelstore, only two zettel have visibility ""public"".
The first zettel is the zettel that contains CSS for displaying the web interface.
This is to ensure that the web interface looks nice even for not authenticated users.
The other zettel is the zettel containing the [[version|00000000000001]] of the Zettelstore.

Please note: if authentication is not enabled, every user has the same rights as the owner of a Zettelstore.
This is also true, if the Zettelstore runs additionally in [[read-only mode|00001004010000#read-only-mode]].
In this case, the [[runtime configuration zettel|00001004020000]] is shown (its visibility is ""owner"").
The [[start-up configuration|00001004010000]] is not shown, because the associated computed zettel with identifier ''00000000000099'' is stored with the visibility ""expert"".
If you want to show such a zettel, you must set ''expert-mode'' to true.

Changes to docs/manual/00001010070300.zettel.


1
2
3
4
5
6
7

title: User roles
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk

Every user is associated with some basic privileges.
These are specified in the user zettel with the key ''user-role''.
>







1
2
3
4
5
6
7
8
id: 00001010070300
title: User roles
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk

Every user is associated with some basic privileges.
These are specified in the user zettel with the key ''user-role''.

Changes to docs/manual/00001010070400.zettel.


1
2
3
4
5
6
7

title: Authorization and read-only mode
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
role: manual

It is possible to enable both the read-only mode of the Zettelstore //and// authentication/authorization.
Both modes are independent from each other.
>







1
2
3
4
5
6
7
8
id: 00001010070400
title: Authorization and read-only mode
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
role: manual

It is possible to enable both the read-only mode of the Zettelstore //and// authentication/authorization.
Both modes are independent from each other.

Changes to docs/manual/00001010070600.zettel.


1
2
3
4
5
6
7

title: Access rules
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
role: manual

Whether an operation of the Zettelstore is allowed or rejected, depends on various factors.

>







1
2
3
4
5
6
7
8
id: 00001010070600
title: Access rules
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
role: manual

Whether an operation of the Zettelstore is allowed or rejected, depends on various factors.

42
43
44
45
46
47
48
49
50
51
52
* Rename a zettel
** Reject the access.
   Only the owner of the Zettelstore is currently allowed to give a new identifier for a zettel.
* Delete a zettel
** Reject the access.
   Only the owner of the Zettelstore is allowed to delete a zettel.
   This may change in the future.
* Reload internal values
** Reject the access.
   Only the owner of the Zettelstore is allowed to perform a reload operation.
   This may change in the future.







<
<
<
<
43
44
45
46
47
48
49




* Rename a zettel
** Reject the access.
   Only the owner of the Zettelstore is currently allowed to give a new identifier for a zettel.
* Delete a zettel
** Reject the access.
   Only the owner of the Zettelstore is allowed to delete a zettel.
   This may change in the future.




Changes to docs/manual/00001010090100.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001010090100
title: External server to encrypt message transport
role: manual
tags: #configuration #encryption #manual #security #zettelstore
syntax: zmk
modified: 20210125195546

Since Zettelstore does not encrypt the messages it exchanges with its clients, you may need some additional software to enable encryption.

=== Public-key encryption
To enable encryption, you probably use some kind of encryption keys.
In most cases, you need to deploy a ""public-key encryption"" process, where your side publish a public encryption key that only works with a corresponding private decryption key.
Technically, this is not trivial.





<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001010090100
title: External server to encrypt message transport
role: manual
tags: #configuration #encryption #manual #security #zettelstore
syntax: zmk


Since Zettelstore does not encrypt the messages it exchanges with its clients, you may need some additional software to enable encryption.

=== Public-key encryption
To enable encryption, you probably use some kind of encryption keys.
In most cases, you need to deploy a ""public-key encryption"" process, where your side publish a public encryption key that only works with a corresponding private decryption key.
Technically, this is not trivial.
63
64
65
66
67
68
69
70
71
    reverse_proxy localhost:23123
  }
}
```
This will forwards requests with the prefix ""/manual"" to the running Zettelstore.
All other requests will be handled by Caddy itself.

In this case you must specify the start-tp configuration key ''url-prefix'' with the value ""/manual"".
This is to allow the Zettelstore to give you the correct URLs with the given prefix.







|

62
63
64
65
66
67
68
69
70
    reverse_proxy localhost:23123
  }
}
```
This will forwards requests with the prefix ""/manual"" to the running Zettelstore.
All other requests will be handled by Caddy itself.

In this case you must specify the start-up configuration key ''url-prefix'' with the value ""/manual"".
This is to allow the Zettelstore to give you the correct URLs with the given prefix.

Changes to docs/manual/00001012000000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210112113014

The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore.
Most integration with other systems and services is done through the API.
The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore.

=== Background
The API is HTTP-based and uses JSON as its main encoding format for exchanging messages between a Zettelstore and its client software.





<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk


The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore.
Most integration with other systems and services is done through the API.
The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore.

=== Background
The API is HTTP-based and uses JSON as its main encoding format for exchanging messages between a Zettelstore and its client software.
29
30
31
32
33
34
35
36
37
38
39
40
41
42


43
44
45

=== Zettel lists
* [[List metadata of all zettel|00001012051200]]
* [[List all zettel, but in different encoding formats|00001012051400]]
* [[List all zettel, but include different parts of a zettel|00001012051600]]
* [[Shape the list of zettel metadata with filter options|00001012051800]]
* [[Sort the list of zettel metadata|00001012052000]]
* List all [[tags|00001006020000]] used in a Zettelstore
* List all [[roles|00001006020100]] used in a Zettelstore.

=== Working with zettel
* Create a new zettel
* [[Retrieve metadata and content of an existing zettel|00001012053400]]
* [[Retrieve references of an existing zettel|00001012053600]]


* Update metadata and content of a zettel
* Rename a zettel
* Delete a zettel







|
|





>
>



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

=== Zettel lists
* [[List metadata of all zettel|00001012051200]]
* [[List all zettel, but in different encoding formats|00001012051400]]
* [[List all zettel, but include different parts of a zettel|00001012051600]]
* [[Shape the list of zettel metadata with filter options|00001012051800]]
* [[Sort the list of zettel metadata|00001012052000]]
* [[List all tags|00001012052200]]
* [[List all roles|00001012052400]]

=== Working with zettel
* Create a new zettel
* [[Retrieve metadata and content of an existing zettel|00001012053400]]
* [[Retrieve references of an existing zettel|00001012053600]]
* [[Retrieve context of an existing zettel|00001012053800]]
* [[Retrieve zettel order within an existing zettel|00001012054000]]
* Update metadata and content of a zettel
* Rename a zettel
* Delete a zettel

Changes to docs/manual/00001012050200.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id: 00001012050200
title: API: Authenticate a client
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210111190943

Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]].
This token has to be used for other API calls.
It is valid for a relatively short amount of time, as configured with the key ''token-timeout-api'' of the [[start-up configuration|00001004010000]] (typically 10 minutes).

The simplest way is to send user identification (''IDENT'') and password (''PASSWORD'') via [[HTTP Basic Authentication|https://tools.ietf.org/html/rfc7617]] and send them to the endpoint ''/a'' with a POST request:
```sh
# curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600}
```

Some tools, like [[curl|https://curl.haxx.se/]], also allow to specify user identification and password as part of the URL:
```sh





<





|







1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
id: 00001012050200
title: API: Authenticate a client
role: manual
tags: #api #manual #zettelstore
syntax: zmk


Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]].
This token has to be used for other API calls.
It is valid for a relatively short amount of time, as configured with the key ''token-timeout-api'' of the [[start-up configuration|00001004010000]] (typically 10 minutes).

The simplest way is to send user identification (''IDENT'') and password (''PASSWORD'') via [[HTTP Basic Authentication|https://tools.ietf.org/html/rfc7617]] and send them to the [[endpoint|00001012920000]] ''/a'' with a POST request:
```sh
# curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600}
```

Some tools, like [[curl|https://curl.haxx.se/]], also allow to specify user identification and password as part of the URL:
```sh

Changes to docs/manual/00001012050400.zettel.


1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

title: API: Renew an access token

tags: #api #manual #zettelstore
syntax: zmk
role: manual

An access token is only valid for a certain duration.
Since the [[authentication process|00001012050200]] will need some processing time, there is a way to renew the token without providing full authentication data.

Send a HTTP PUT request to the endpoint ''/a'' and include the current access token in the ''Authorization'' header:

```sh
# curl -X PUT -H 'Authorization: Bearer TOKEN' http://127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":456}
```
You may receive a new access token, or the current one if it was obtained not a long time ago.
However, the lifetime of the returned [[access token|00001012921000]] is accurate.
>

>


<




|







1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
id: 00001012050400
title: API: Renew an access token
role: manual
tags: #api #manual #zettelstore
syntax: zmk


An access token is only valid for a certain duration.
Since the [[authentication process|00001012050200]] will need some processing time, there is a way to renew the token without providing full authentication data.

Send a HTTP PUT request to the [[endpoint|00001012920000]] ''/a'' and include the current access token in the ''Authorization'' header:

```sh
# curl -X PUT -H 'Authorization: Bearer TOKEN' http://127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":456}
```
You may receive a new access token, or the current one if it was obtained not a long time ago.
However, the lifetime of the returned [[access token|00001012921000]] is accurate.

Changes to docs/manual/00001012050600.zettel.


1
2
3
4
5
6
7

title: API: Provide an access token
tags: #api #manual #zettelstore
syntax: zmk
role: manual

The [[authentication process|00001012050200]] provides you with an [[access token|00001012921000]].
Most API calls need such an access token, so that they know the identity of the caller.
>







1
2
3
4
5
6
7
8
id: 00001012050600
title: API: Provide an access token
tags: #api #manual #zettelstore
syntax: zmk
role: manual

The [[authentication process|00001012050200]] provides you with an [[access token|00001012921000]].
Most API calls need such an access token, so that they know the identity of the caller.

Changes to docs/manual/00001012051200.zettel.


1

2
3
4
5
6
7
8
9
10
11
12
13

title: API: List metadata of all zettel

tags: #api #manual #zettelstore
syntax: zmk
role: manual

To list the metadata of all zettel just send a HTTP GET request to the endpoint ''/z''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/z
{"list":[{"id":"00001012051200","url":"/z/00001012051200","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050600","url":"/z/00001012050600","meta":{"title":"API: Provide an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050400","url":"/z/00001012050400","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050200","url":"/z/00001012050200","meta":{"title":"API: Authenticate a client","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012000000","url":"/z/00001012000000","meta":{"title":"API","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}}]}
```

>

>


<

|







1
2
3
4
5

6
7
8
9
10
11
12
13
14
id: 00001012051200
title: API: List metadata of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk


To list the metadata of all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/z
{"list":[{"id":"00001012051200","url":"/z/00001012051200","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050600","url":"/z/00001012050600","meta":{"title":"API: Provide an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050400","url":"/z/00001012050400","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050200","url":"/z/00001012050200","meta":{"title":"API: Authenticate a client","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012000000","url":"/z/00001012000000","meta":{"title":"API","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}}]}
```

Changes to docs/manual/00001012051400.zettel.


1
2
3
4
5
6
7

title: API: List all zettel, but in different encoding formats
tags: #api #manual #zettelstore
syntax: zmk
role: manual

You can add a query parameter ''_format=[[FORMAT|00001012920500]]'' to select the encoding format when [[retrieving all zettel|00001012051200]].
Probably some formats are not very useful and may not make sense.
>







1
2
3
4
5
6
7
8
id: 00001012051400
title: API: List all zettel, but in different encoding formats
tags: #api #manual #zettelstore
syntax: zmk
role: manual

You can add a query parameter ''_format=[[FORMAT|00001012920500]]'' to select the encoding format when [[retrieving all zettel|00001012051200]].
Probably some formats are not very useful and may not make sense.

Changes to docs/manual/00001012051600.zettel.


1
2
3
4
5
6
7

title: API: List all zettel, but include different parts of a zettel
tags: #api #manual #zettelstore
syntax: zmk
role: manual

For JSON-based formats[^[[''json''|00001012920501]] and [[''djson''|00001012920503]]] you can add a query parameter ''_part=[[PART|00001012920800]]'' to select which parts of a zettel must be encoded.

>







1
2
3
4
5
6
7
8
id: 00001012051600
title: API: List all zettel, but include different parts of a zettel
tags: #api #manual #zettelstore
syntax: zmk
role: manual

For JSON-based formats[^[[''json''|00001012920501]] and [[''djson''|00001012920503]]] you can add a query parameter ''_part=[[PART|00001012920800]]'' to select which parts of a zettel must be encoded.

Changes to docs/manual/00001012051800.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001012051800
title: API: Shape the list of zettel metadata with filter options
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210112114019

In most cases, it is not essential to list //all// zettel.
Typically, you are interested only in a subset of the zettel maintained by your Zettelstore.
This is done by adding some query parameters to the general ''GET /z'' request.

=== Filter
Every query parameter that does //not// start with the low line character (""_"", ''U+005F'') is treated as the name of a [[metadata|00001006010000]] key.
According to the [[type|00001006030000]] of a metadata key, zettel are matched and therefore filtered.
All [[supported|00001006020000]] metadata keys have a well-defined type.
User-defined keys have the type ''e'' (string, possibly empty).

For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be:
```sh
# curl 'http://127.0.0.1:23123/z?title=API'





<






|







1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
id: 00001012051800
title: API: Shape the list of zettel metadata with filter options
role: manual
tags: #api #manual #zettelstore
syntax: zmk


In most cases, it is not essential to list //all// zettel.
Typically, you are interested only in a subset of the zettel maintained by your Zettelstore.
This is done by adding some query parameters to the general ''GET /z'' request.

=== Filter
Every query parameter that does //not// begin with the low line character (""_"", ''U+005F'') is treated as the name of a [[metadata|00001006010000]] key.
According to the [[type|00001006030000]] of a metadata key, zettel are matched and therefore filtered.
All [[supported|00001006020000]] metadata keys have a well-defined type.
User-defined keys have the type ''e'' (string, possibly empty).

For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be:
```sh
# curl 'http://127.0.0.1:23123/z?title=API'
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

=== Limit and offset
By using the query parameter ""''_limit''"", which must have an integer value, you specifying an upper limit of list elements:
```sh
# curl 'http://192.168.17.7:23121/z?title=API&_part=id&_sort=id&_limit=2'
{"list":[{"id":"00001012000000","url":"/z/00001012000000"},{"id":"00001012050200","url":"/z/00001012050200"}]}
```
The query parameter ""''_offset''"" allows to list not only the first elements, but start at a specific element:
```sh
# curl 'http://192.168.17.7:23121/z?title=API&_part=id&_sort=id&_limit=2&_offset=1'
{"list":[{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012050400","url":"/z/00001012050400"}]}
```

=== General filter
The query parameter ""''_s''"" allows to provide a string, which will be searched for in all metadata.
While searching, the [[type|00001006030000]] of each metadata key will be respected.

You are allowed to specify this query parameter more than once.
All results will be intersected, i.e. a zettel will be included into the list if both of the provided values match.

This parameter loosely resembles the search box of the web user interface.







|













37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

=== Limit and offset
By using the query parameter ""''_limit''"", which must have an integer value, you specifying an upper limit of list elements:
```sh
# curl 'http://192.168.17.7:23121/z?title=API&_part=id&_sort=id&_limit=2'
{"list":[{"id":"00001012000000","url":"/z/00001012000000"},{"id":"00001012050200","url":"/z/00001012050200"}]}
```
The query parameter ""''_offset''"" allows to list not only the first elements, but to begin at a specific element:
```sh
# curl 'http://192.168.17.7:23121/z?title=API&_part=id&_sort=id&_limit=2&_offset=1'
{"list":[{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012050400","url":"/z/00001012050400"}]}
```

=== General filter
The query parameter ""''_s''"" allows to provide a string, which will be searched for in all metadata.
While searching, the [[type|00001006030000]] of each metadata key will be respected.

You are allowed to specify this query parameter more than once.
All results will be intersected, i.e. a zettel will be included into the list if both of the provided values match.

This parameter loosely resembles the search box of the web user interface.

Changes to docs/manual/00001012052000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001012052000
title: API: Sort the list of zettel metadata
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20210112113839

If not specified, the list of zettel is sorted descending by the value of the zettel identifier.
The highest zettel identifier, which is a number, comes first.
You change that with the ""''_sort''"" query parameter.
Alternatively, you can also use the ""''_order''"" query parameter.
It is an alias.






<







1
2
3
4
5

6
7
8
9
10
11
12
id: 00001012052000
title: API: Sort the list of zettel metadata
role: manual
tags: #api #manual #zettelstore
syntax: zmk


If not specified, the list of zettel is sorted descending by the value of the zettel identifier.
The highest zettel identifier, which is a number, comes first.
You change that with the ""''_sort''"" query parameter.
Alternatively, you can also use the ""''_order''"" query parameter.
It is an alias.

Added docs/manual/00001012052200.zettel.





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id: 00001012052200
title: API: List all tags
role: manual
tags: #api #manual #zettelstore
syntax: zmk

To list all [[tags|00001006020000#tags]] used in the Zettelstore just send a HTTP GET request to the [[endpoint|00001012920000]] ''/t''.
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/t
{"tags":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}}
```

The JSON object only contains the key ''"tags"'' with the value of another object.
This second object contains all tags as keys and the list of identifier of those zettel with this tag as a value.

Please note that this structure will likely change in the future to be more compliant with other API calls.

Added docs/manual/00001012052400.zettel.





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id: 00001012052400
title: API: List all roles
role: manual
tags: #api #manual #zettelstore
syntax: zmk

To list all [[roles|00001006020100]] used in the Zettelstore just send a HTTP GET request to the [[endpoint|00001012920000]] ''/r''.
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/r
{"role-list":["configuration","manual","user","zettel"]}
```

The JSON object only contains the key ''"role-list"'' with the value of a sorted string list.
Each string names one role.

Please note that this structure will likely change in the future to be more compliant with other API calls.

Changes to docs/manual/00001012053400.zettel.


1

2
3
4
5
6
7
8
9
10
11
12
13

title: API: Retrieve metadata and content of an existing zettel

tags: #api #manual #zettelstore
syntax: zmk
role: manual

The endpoint to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053400''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/z/00001012053400
{"id":"00001012053400","url":"/z/00001012053400","meta":{"title":"API: Retrieve data for an exisiting zettel","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual","copyright":"(c) 2020 by Detlef Stern <ds@zettelstore.de>","lang":"en","license":"CC BY-SA 4.0"},"content":"The endpoint to work with a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).\n\nFor example, ...
```
>

>


<

|







1
2
3
4
5

6
7
8
9
10
11
12
13
14
id: 00001012053400
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk


The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053400''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/z/00001012053400
{"id":"00001012053400","url":"/z/00001012053400","meta":{"title":"API: Retrieve data for an exisiting zettel","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual","copyright":"(c) 2020 by Detlef Stern <ds@zettelstore.de>","lang":"en","license":"CC BY-SA 4.0"},"content":"The endpoint to work with a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).\n\nFor example, ...
```

Changes to docs/manual/00001012053600.zettel.


1
2
3
4
5
6
7

title: API: Retrieve references of an existing zettel
tags: #api #manual #zettelstore
syntax: zmk
role: manual

The web of zettel is one important value of a Zettelstore.
Many zettel references other zettel, images, external/local material or, via citations, external literature.
>







1
2
3
4
5
6
7
8
id: 00001012053600
title: API: Retrieve references of an existing zettel
tags: #api #manual #zettelstore
syntax: zmk
role: manual

The web of zettel is one important value of a Zettelstore.
Many zettel references other zettel, images, external/local material or, via citations, external literature.

Added docs/manual/00001012053800.zettel.

































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
id: 00001012053800
title: API: Retrieve context of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk

The context of an origin zettel consists of those zettel that are somehow connected to the origin zettel.
Direct connections of an origin zettel to other zettel are visible via [[metadata values|00001006020000]], such as ''backward'', ''forward'' or other values with type [[identifier|00001006032000]] or [[set of identifier|00001006032500]].
Zettel are also connected by using same [[tags|00001006020000#tags]].

The context is defined by a //direction//, a //depth//, and a /limit//:
* Direction: connections are directed.
  For example, the metadata value of ''backward'' lists all zettel that link to the current zettel, while ''formward'' list all zettel to which the current zettel links.
  When you are only interested in one direction, set the parameter ''dir'' either to the value ""backward"" or ""forward"".
  All other values, including a missing value, is interpreted as ""both"".
* Depth: a direct connection has depth 1, an indirect connection is the length of the shortest path between two zettel.
  You should limit the depth by using the parameter ''depth''.
  Its default value is ""5"".
  A value of ""0"" does disable any depth check.
* Limit: to set an upper bound for the returned context, you should use the parameter ''limit''.
  Its default value is ""200"".
  A value of ""0"" disables does not limit the number of elements returned.

Zettel with same tags as the origin zettel are considered depth 1.
Only for the origin zettel, tags are used to calculate a connection.
Currently, only some of the newest zettel with a given tag are considered a connection.[^The number of zettel is given by the value of parameter ''depth''.]
Otherwise the context would become too big and therefore unusable.

To retrieve the context of an existing zettel, use the [[endpoint|00001012920000]] ''/y/{ID}''.

````
# curl 'http://127.0.0.1:23123/y/00001012053800?limit=3&dir=forward&depth=2'
{"id": "00001012053800","url": "/z/00001012053800","meta": {...},"list": [{"id": "00001012921000","url": "/z/00001012921000","meta": {...}},{"id": "00001012920800","url": "/z/00001012920800","meta": {...}},{"id": "00010000000000","url": "/z/00010000000000","meta": {...}}]}
````
Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.]
````json
{
  "id": "00001012053800",
  "url": "/z/00001012053800",
  "meta": {...},
  "list": [
    {
      "id": "00001012921000",
      "url": "/z/00001012921000",
      "meta": {...}
    },
    {
      "id": "00001012920800",
      "url": "/z/00001012920800",
      "meta": {...}
    },
    {
      "id": "00010000000000",
      "url": "/z/00010000000000",
      "meta": {...}
    }
  ]
}
````
=== Keys
The following top-level JSON keys are returned:
; ''id''
: The zettel identifier for which the context was requested.
; ''url''
: The API endpoint to fetch more information about the zettel.
; ''meta'':
: The metadata of the zettel, encoded as a JSON object.
; ''list''
: A list of JSON objects with keys ''id'', ''url'' and ''meta'' that contains the zettel of the context.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
; ''400''
: Request was not valid.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Added docs/manual/00001012054000.zettel.





































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
id: 00001012054000
title: API: Retrieve zettel order within an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk

Some zettel act as a ""table of contents"" for other zettel.
The [[Home zettel|00010000000000]] of this manual is one example, the [[general API description|00001012000000]] is another.
Every zettel with a certain internal structure can act as the ""table of contents"" for others.

What is a ""table of contents""?
Basically, it is just a list of references to other zettel.

To retrieve the ""table of contents"", the software looks at first level [[list items|00001007030200]].
If an item contains a valid reference to a zettel, this reference will be interpreted as an item in the table of contents.

This applies only to first level list items (ordered or unordered list), but not to deeper levels.
Only the first reference to a valid zettel is collected for the table of contents.
Following references to zettel within such an list item are ignored.

To retrieve the zettel order of an existing zettel, use the [[endpoint|00001012920000]] ''/o/{ID}''.

````
# curl http://127.0.0.1:23123/o/00010000000000
{"id":"00010000000000","url":"/z/00010000000000","meta":{...},"list":[{"id":"00001001000000","url":"/z/00001001000000","meta":{...}},{"id":"00001002000000","url":"/z/00001002000000","meta":{...}},{"id":"00001003000000","url":"/z/00001003000000","meta":{...}},{"id":"00001004000000","url":"/z/00001004000000","meta":{...}},...,{"id":"00001014000000","url":"/z/00001014000000","meta":{...}}]}
````
Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.]
````json
{
  "id": "00010000000000",
  "url": "/z/00010000000000",
  "order": [
    {
      "id": "00001001000000",
      "url": "/z/00001001000000",
      "meta": {...}
    },
    {
      "id": "00001002000000",
      "url": "/z/00001002000000",
      "meta": {...}
    },
    {
      "id": "00001003000000",
      "url": "/z/00001003000000",
      "meta": {...}
    },
    {
      "id": "00001004000000",
      "url": "/z/00001004000000",
      "meta": {...}
    },
    ...
    {
      "id": "00001014000000",
      "url": "/z/00001014000000",
      "meta": {...}
    }
  ]
}
````
=== Kind
The following top-level JSON keys are returned:
; ''id''
: The zettel identifier for which the references were requested.
; ''url''
: The API endpoint to fetch more information about the zettel.
; ''meta'':
: The metadata of the zettel, encoded as a JSON object.
; ''list''
: A list of JSON objects with keys ''id'', ''url'', and ''meta'' that describe other zettel in the defined order.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
; ''400''
: Request was not valid.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Changes to docs/manual/00001012920000.zettel.


1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19




20
21
22
23
24
25
26
27

title: Endpoints used by the API

tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

All API endpoints conform to the pattern ''[PREFIX]/LETTER[/ZETTEL-ID]'', where:
; ''PREFIX''
: is an optional URL prefix, configured via the ''url-prefix'' [[startup configuration|00001004010000]],
; ''LETTER''
: is a single letter that specifies the ressource type,
; ''ZETTEL-ID''
: is an optional 14 digits string that uniquely [[identify a zettel|00001006050000]].

The following letters are currently in use:

|= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]]
| ''a'' | POST: [[Client authentication|00001012050200]] |
|       | PUT: [[renew access token|00001012050400]] |
| ''l'' |  | GET: [[list references|00001012053600]]




| ''z'' | GET: [[list zettel|00001012051200]] | GET: [[retrieve zettel|00001012053400]]
|       | POST: add new zettel | PUT: change a zettel
|       |  | DELETE: delete the zettel

The full URL will contain either the ''http'' oder ''https'' scheme, a host name, and an optional port number.

The API examples will assume the ''http'' schema, the local host ''127.0.0.1'', the default port ''23123'', and the default empty ''PREFIX''.
Therefore, all URLs will start with ''http://127.0.0.1:23123''.
>

>


<



|











>
>
>
>







|
1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
id: 00001012920000
title: Endpoints used by the API
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk


All API endpoints conform to the pattern ''[PREFIX]/LETTER[/ZETTEL-ID]'', where:
; ''PREFIX''
: is an optional URL prefix, configured via the ''url-prefix'' [[start-up configuration|00001004010000]],
; ''LETTER''
: is a single letter that specifies the ressource type,
; ''ZETTEL-ID''
: is an optional 14 digits string that uniquely [[identify a zettel|00001006050000]].

The following letters are currently in use:

|= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]]
| ''a'' | POST: [[Client authentication|00001012050200]] |
|       | PUT: [[renew access token|00001012050400]] |
| ''l'' |  | GET: [[list references|00001012053600]]
| ''o'' |  | GET: [[list zettel order|00001012054000]]
| ''r'' | GET: [[list roles|00001012052400]]
| ''t'' | GET: [[list tags|00001012052200]]
| ''y'' |  | GET: [[list zettel context|00001012053800]]
| ''z'' | GET: [[list zettel|00001012051200]] | GET: [[retrieve zettel|00001012053400]]
|       | POST: add new zettel | PUT: change a zettel
|       |  | DELETE: delete the zettel

The full URL will contain either the ''http'' oder ''https'' scheme, a host name, and an optional port number.

The API examples will assume the ''http'' schema, the local host ''127.0.0.1'', the default port ''23123'', and the default empty ''PREFIX''.
Therefore, all URLs in the API documentation will begin with ''http://127.0.0.1:23123''.

Changes to docs/manual/00001012920500.zettel.


1
2
3
4
5
6
7

title: Formats available by the API
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

A zettel representation can be encoded in various formats for further processing.

>







1
2
3
4
5
6
7
8
id: 00001012920500
title: Formats available by the API
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

A zettel representation can be encoded in various formats for further processing.

Changes to docs/manual/00001012920501.zettel.


1
2
3
4
5
6
7

title: JSON Format
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk

This is the default representation of a zettel or a list of zettel.
Basically, user provided data is encoded as a string (zettel content and metadata values),
>







1
2
3
4
5
6
7
8
id: 00001012920501
title: JSON Format
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk

This is the default representation of a zettel or a list of zettel.
Basically, user provided data is encoded as a string (zettel content and metadata values),
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
* ''"encoding"'' and ''"content"'': the actual content of the zettel. See below for details.

''"id"'' and ''"url"'' are always sent to the client.
It depends on the value of the required [[zettel part|00001012920800]], whether ''"meta"'' or ''"content"'' or both are sent.

For an example, take a look at the JSON encoding of this page, which is available via the ""Info"" sub-page of this zettel: 

* [[../z/00001012920501?_part=id]],
* [[../z/00001012920501?_part=zettel]],
* [[../z/00001012920501?_part=meta]],
* [[../z/00001012920501?_part=content]].

If transferred via HTTP, the content type will be ''application/json''.

=== Metadata
This ia a JSON object, that maps [[metadata keys|00001006010000]] to their values.
Their values are encoded as strings, even if they contain a number (or something else).








|
|
|
|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
* ''"encoding"'' and ''"content"'': the actual content of the zettel. See below for details.

''"id"'' and ''"url"'' are always sent to the client.
It depends on the value of the required [[zettel part|00001012920800]], whether ''"meta"'' or ''"content"'' or both are sent.

For an example, take a look at the JSON encoding of this page, which is available via the ""Info"" sub-page of this zettel: 

* [[//z/00001012920501?_part=id]],
* [[//z/00001012920501?_part=zettel]],
* [[//z/00001012920501?_part=meta]],
* [[//z/00001012920501?_part=content]].

If transferred via HTTP, the content type will be ''application/json''.

=== Metadata
This ia a JSON object, that maps [[metadata keys|00001006010000]] to their values.
Their values are encoded as strings, even if they contain a number (or something else).

Changes to docs/manual/00001012920503.zettel.


1
2
3
4
5
6
7

title: DJSON Format
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk

A zettel representation that allows to process the syntactic structure of a zettel.
It is a JSON-based encoding format, but different to [[json|00001012920501]].
>







1
2
3
4
5
6
7
8
id: 00001012920503
title: DJSON Format
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk

A zettel representation that allows to process the syntactic structure of a zettel.
It is a JSON-based encoding format, but different to [[json|00001012920501]].

Changes to docs/manual/00001012920510.zettel.


1
2
3
4
5
6
7
8
9
10
11

title: HTML Format
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

A zettel representation in HTML.
This representation is different form the [[web user interface|00001014000000]] as it contains the zettel representation only and no additional data such as the menu bar.

It is intended to be used by external clients.

If transferred via HTTP, the content type will be ''text/html''.
>











1
2
3
4
5
6
7
8
9
10
11
12
id: 00001012920510
title: HTML Format
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

A zettel representation in HTML.
This representation is different form the [[web user interface|00001014000000]] as it contains the zettel representation only and no additional data such as the menu bar.

It is intended to be used by external clients.

If transferred via HTTP, the content type will be ''text/html''.

Changes to docs/manual/00001012920513.zettel.


1
2
3
4
5
6
7
8
9
10
11

title: Native Format
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

A zettel representation shows the structure of a zettel in a more user-friendly way.
Mostly used for debugging.

If transferred via HTTP, the content type will be ''text/plain''.

TODO: formal description
>











1
2
3
4
5
6
7
8
9
10
11
12
id: 00001012920513
title: Native Format
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

A zettel representation shows the structure of a zettel in a more user-friendly way.
Mostly used for debugging.

If transferred via HTTP, the content type will be ''text/plain''.

TODO: formal description

Changes to docs/manual/00001012920516.zettel.


1
2
3
4
5
6
7

title: Raw Format
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

A zettel representation as it was loaded from the zettel content.

>







1
2
3
4
5
6
7
8
id: 00001012920516
title: Raw Format
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

A zettel representation as it was loaded from the zettel content.

Changes to docs/manual/00001012920519.zettel.


1
2
3
4
5
6
7
8
9
10
11

title: Text Format
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

A zettel representation contains just all textual data of a zettel.
Could be used for creating a search index.

Every line may contain zero, one, or more words, spearated by space character.

If transferred via HTTP, the content type will be ''text/plain''.
>











1
2
3
4
5
6
7
8
9
10
11
12
id: 00001012920519
title: Text Format
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

A zettel representation contains just all textual data of a zettel.
Could be used for creating a search index.

Every line may contain zero, one, or more words, spearated by space character.

If transferred via HTTP, the content type will be ''text/plain''.

Changes to docs/manual/00001012920522.zettel.


1
2
3
4
5
6
7
8
9

title: Zmk Format
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

A zettel representation that tries to recreate a [[Zettelmarkup|00001007000000]] representation of the zettel.
Useful if you want to convert [[other markup languages|00001008000000]] to Zettelmarkup (e.g. Markdown).

If transferred via HTTP, the content type will be ''text/plain''.
>









1
2
3
4
5
6
7
8
9
10
id: 00001012920522
title: Zmk Format
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

A zettel representation that tries to recreate a [[Zettelmarkup|00001007000000]] representation of the zettel.
Useful if you want to convert [[other markup languages|00001008000000]] to Zettelmarkup (e.g. Markdown).

If transferred via HTTP, the content type will be ''text/plain''.

Changes to docs/manual/00001012920800.zettel.


1
2
3
4
5
6
7

title: Values to specify zettel parts
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

When working with [[zettel|00001006000000]], you could work with the whole zettel, with its metadata, or with its content:
; [!zettel]''zettel''
>







1
2
3
4
5
6
7
8
id: 00001012920800
title: Values to specify zettel parts
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

When working with [[zettel|00001006000000]], you could work with the whole zettel, with its metadata, or with its content:
; [!zettel]''zettel''

Changes to docs/manual/00001012921000.zettel.


1
2
3
4
5
6
7
8
9
10
11
12
13

title: API: JSON structure of an access token
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

If the [[authentiction process|00001012050200]] was successful, an access token with some additional data is returned.
The same is true, if the access token was [[renewed|00001012050400]].
The response is structured as an JSON object, with the following named values:

|=Name|Description
|''access_token''|The access token itself, as string value, which is a [[JSON Web Token|https://tools.ietf.org/html/rfc7519]] (JWT, RFC 7915)
|''token_type''|The type of the token, always set to ''"Bearer"'', as described in [[RFC 6750|https://tools.ietf.org/html/rfc6750]]
|''expires_in''|An integer that gives a hint about the lifetime / endurance of the token, measured in seconds
>













1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001012921000
title: API: JSON structure of an access token
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

If the [[authentiction process|00001012050200]] was successful, an access token with some additional data is returned.
The same is true, if the access token was [[renewed|00001012050400]].
The response is structured as an JSON object, with the following named values:

|=Name|Description
|''access_token''|The access token itself, as string value, which is a [[JSON Web Token|https://tools.ietf.org/html/rfc7519]] (JWT, RFC 7915)
|''token_type''|The type of the token, always set to ''"Bearer"'', as described in [[RFC 6750|https://tools.ietf.org/html/rfc6750]]
|''expires_in''|An integer that gives a hint about the lifetime / endurance of the token, measured in seconds

Changes to docs/manual/00001014000000.zettel.


1
2
3
4
5
6
7

title: Web user interface
tags: #manual #webui #zettelstore
syntax: zmk
role: manual

The Web user interface is just a secondary way to interact with a Zettelstore.
Using external software that interacts via the [[API|00001012000000]] is the recommended way.
>







1
2
3
4
5
6
7
8
id: 00001014000000
title: Web user interface
tags: #manual #webui #zettelstore
syntax: zmk
role: manual

The Web user interface is just a secondary way to interact with a Zettelstore.
Using external software that interacts via the [[API|00001012000000]] is the recommended way.

Added docs/manual/00010000000000.zettel.











































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
id: 00001000000000
title: Zettelstore Manual
role: manual
tags: #manual #zettelstore
syntax: zmk

* [[Introduction|00001001000000]]
* [[Design goals|00001002000000]]
* [[Installation|00001003000000]]
* [[Configuration|00001004000000]]
* [[Structure of Zettelstore|00001005000000]]
* [[Layout of a zettel|00001006000000]]
* [[Zettelmarkup|00001007000000]]
* [[Other markup languages|00001008000000]]
* [[Security|00001010000000]]
* [[API|00001012000000]]
* [[Web user interface|00001014000000]]
* Troubleshooting
* Frequently asked questions

Licensed under the EUPL-1.2-or-later.

Changes to domain/id/id.go.

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27



28
29
30


31
32
33
34
35
36
37
38

39
40


41
42


43

44
45
46
47
48
49
50
51
52
53
54
//-----------------------------------------------------------------------------

// Package id provides domain specific types, constants, and functions about
// zettel identifier.
package id

import (
	"sort"
	"strconv"
	"time"
)

// 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
	ConfigurationZid  = Zid(100)


	BaseTemplateZid   = Zid(10100)
	LoginTemplateZid  = Zid(10200)
	ListTemplateZid   = Zid(10300)
	DetailTemplateZid = Zid(10401)
	InfoTemplateZid   = Zid(10402)
	FormTemplateZid   = Zid(10403)
	RenameTemplateZid = Zid(10404)
	DeleteTemplateZid = Zid(10405)

	RolesTemplateZid  = Zid(10500)
	TagsTemplateZid   = Zid(10600)


	BaseCSSZid        = Zid(20001)



	// Range 90000...99999 is reserved for zettel templates

	TemplateNewZettelZid = Zid(91001)
	TemplateNewUserZid   = Zid(96001)

	WelcomeZid = Zid(19700101000000)
)

const maxZid = 99999999999999

// Parse interprets a string as a zettel identification and
// returns its integer value.
func Parse(s string) (Zid, error) {







<










|
>
>
>

|
|
>
>
|
|
|
|
|
|
|
|
>
|
|
>
>
|

>
>

>
|
|

|







9
10
11
12
13
14
15

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//-----------------------------------------------------------------------------

// Package id provides domain specific types, constants, and functions about
// zettel identifier.
package id

import (

	"strconv"
	"time"
)

// 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.
// Note: if you change some values, ensure that you also change them in the
//       constant place. They are mentioned there literally, because these
//       constants are not available there.
const (
	Invalid          = Zid(0) // Invalid is a Zid that will never be valid
	ConfigurationZid = Zid(100)

	// WebUI HTML templates are in the range 10000..19999
	BaseTemplateZid    = Zid(10100)
	LoginTemplateZid   = Zid(10200)
	ListTemplateZid    = Zid(10300)
	DetailTemplateZid  = Zid(10401)
	InfoTemplateZid    = Zid(10402)
	FormTemplateZid    = Zid(10403)
	RenameTemplateZid  = Zid(10404)
	DeleteTemplateZid  = Zid(10405)
	ContextTemplateZid = Zid(10406)
	RolesTemplateZid   = Zid(10500)
	TagsTemplateZid    = Zid(10600)

	// WebUI CSS pages are in the range 20000..29999
	BaseCSSZid = Zid(20001)

	// WebUI JS pages are in the range 30000..39999

	// Range 90000...99999 is reserved for zettel templates
	TOCNewTemplateZid    = Zid(90000)
	TemplateNewZettelZid = Zid(90001)
	TemplateNewUserZid   = Zid(90002)

	DefaultHomeZid = Zid(10000000000)
)

const maxZid = 99999999999999

// Parse interprets a string as a zettel identification and
// returns its integer value.
func Parse(s string) (Zid, error) {
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
	}
	res, err := Parse(s)
	if err != nil {
		panic(err)
	}
	return res
}

// Sort a slice of Zids.
func Sort(zids []Zid) {
	sort.Sort(zidSlice(zids))
}

type zidSlice []Zid

func (zs zidSlice) Len() int           { return len(zs) }
func (zs zidSlice) Less(i, j int) bool { return zs[i] < zs[j] }
func (zs zidSlice) Swap(i, j int)      { zs[i], zs[j] = zs[j], zs[i] }







<
<
<
<
<
<
<
<
<
<
<
108
109
110
111
112
113
114











	}
	res, err := Parse(s)
	if err != nil {
		panic(err)
	}
	return res
}











Changes to domain/id/id_test.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 id_test provides unit tests for testing zettel id specific functions.
package id_test

import (
	"testing"

	"zettelstore.de/z/domain/id"
)

func TestParseZettelID(t *testing.T) {
}

func TestIsValid(t *testing.T) {
	validIDs := []string{
		"00000000000001",
		"00000000000020",
		"00000000000300",
		"00000000004000",
		"00000000050000",

|

















<
<
<







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19



20
21
22
23
24
25
26
//-----------------------------------------------------------------------------
// Copyright (c) 2020-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 id_test provides unit tests for testing zettel id specific functions.
package id_test

import (
	"testing"

	"zettelstore.de/z/domain/id"
)




func TestIsValid(t *testing.T) {
	validIDs := []string{
		"00000000000001",
		"00000000000020",
		"00000000000300",
		"00000000004000",
		"00000000050000",

Added domain/id/set.go.





















































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 id provides domain specific types, constants, and functions about
// zettel identifier.
package id

// Set is a set of zettel identifier
type Set map[Zid]bool

// 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)
	for _, zid := range zids {
		result[zid] = true
	}
	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)
	for _, zid := range zids {
		result[zid] = true
	}
	return result
}

// Sort returns the set as a sorted slice of zettel identifier.
func (s Set) Sort() 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
}

Added domain/id/slice.go.









































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//-----------------------------------------------------------------------------
// Copyright (c) 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 id provides domain specific types, constants, and functions about
// zettel identifier.
package id

import (
	"sort"
	"strings"
)

// Slice is a sequence of zettel identifier. A special case is a sorted slice.
type Slice []Zid

func (zs Slice) Len() int           { return len(zs) }
func (zs Slice) Less(i, j int) bool { return zs[i] < zs[j] }
func (zs Slice) Swap(i, j int)      { zs[i], zs[j] = zs[j], zs[i] }

// Sort a slice of Zids.
func (zs Slice) Sort() { sort.Sort(zs) }

// Copy a zettel identifier slice
func (zs Slice) Copy() Slice {
	if zs == nil {
		return nil
	}
	result := make(Slice, len(zs))
	copy(result, zs)
	return result
}

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 domain/id/slice_test.go.



















































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//-----------------------------------------------------------------------------
// 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 id provides domain specific types, constants, and functions about
// zettel identifier.
package id_test

import (
	"testing"

	"zettelstore.de/z/domain/id"
)

func TestSort(t *testing.T) {
	zs := id.Slice{9, 4, 6, 1, 7}
	zs.Sort()
	if zs[0] != 1 || zs[1] != 4 || zs[2] != 6 || zs[3] != 7 || zs[4] != 9 {
		t.Errorf("Slice.Sort did not work. Expected %v, got %v", id.Slice{1, 4, 6, 7, 9}, zs)
	}
}

func TestCopy(t *testing.T) {
	var orig id.Slice
	got := orig.Copy()
	if got != nil {
		t.Errorf("Nil copy resulted in %v", got)
	}
	orig = id.Slice{9, 4, 6, 1, 7}
	got = orig.Copy()
	if len(got) != len(orig) || got[0] != 9 || got[1] != 4 || got[2] != 6 || got[3] != 1 || got[4] != 7 {
		t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got)
	}
}
func TestString(t *testing.T) {
	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)
		}
	}
}

Changes to domain/meta/meta.go.

71
72
73
74
75
76
77








78
79
80
81
82
83
84
// 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
}









// 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: TypeUnknown}







>
>
>
>
>
>
>
>







71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// 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
}

// 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: TypeUnknown}
118
119
120
121
122
123
124

125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
	KeyDefaultTitle      = registerKey("default-title", TypeZettelmarkup, usageUser, "")
	KeyDefaultVisibility = registerKey("default-visibility", TypeWord, usageUser, "")
	KeyDuplicates        = registerKey("duplicates", TypeBool, usageUser, "")
	KeyExpertMode        = registerKey("expert-mode", TypeBool, usageUser, "")
	KeyFolge             = registerKey("folge", TypeIDSet, usageProperty, "")
	KeyFooterHTML        = registerKey("footer-html", TypeString, usageUser, "")
	KeyForward           = registerKey("forward", TypeIDSet, usageProperty, "")

	KeyLang              = registerKey("lang", TypeWord, usageUser, "")
	KeyLicense           = registerKey("license", TypeEmpty, usageUser, "")
	KeyListPageSize      = registerKey("list-page-size", TypeNumber, usageUser, "")
	KeyNewRole           = registerKey("new-role", TypeWord, usageUser, "")
	KeyMarkerExternal    = registerKey("marker-external", TypeEmpty, usageUser, "")
	KeyModified          = registerKey("modified", TypeTimestamp, usageComputed, "")
	KeyPrecursor         = registerKey("precursor", TypeIDSet, usageUser, KeyFolge)
	KeyPublished         = registerKey("published", TypeTimestamp, usageProperty, "")
	KeyReadOnly          = registerKey("read-only", TypeWord, usageUser, "")
	KeySiteName          = registerKey("site-name", TypeString, usageUser, "")
	KeyStart             = registerKey("start", TypeID, usageUser, "")
	KeyURL               = registerKey("url", TypeURL, usageUser, "")
	KeyUserID            = registerKey("user-id", TypeWord, usageUser, "")
	KeyUserRole          = registerKey("user-role", TypeWord, usageUser, "")
	KeyVisibility        = registerKey("visibility", TypeWord, usageUser, "")
	KeyYAMLHeader        = registerKey("yaml-header", TypeBool, usageUser, "")
	KeyZettelFileSyntax  = registerKey("zettel-file-syntax", TypeWordSet, usageUser, "")
)

// Important values for some keys.
const (
	ValueRoleConfiguration = "configuration"
	ValueRoleUser          = "user"
	ValueRoleNewTemplate   = "new-template"
	ValueRoleZettel        = "zettel"
	ValueSyntaxNone        = "none"
	ValueSyntaxZmk         = "zmk"
	ValueTrue              = "true"
	ValueFalse             = "false"
	ValueUserRoleReader    = "reader"
	ValueUserRoleWriter    = "writer"







>



<






<












<







126
127
128
129
130
131
132
133
134
135
136

137
138
139
140
141
142

143
144
145
146
147
148
149
150
151
152
153
154

155
156
157
158
159
160
161
	KeyDefaultTitle      = registerKey("default-title", TypeZettelmarkup, usageUser, "")
	KeyDefaultVisibility = registerKey("default-visibility", TypeWord, usageUser, "")
	KeyDuplicates        = registerKey("duplicates", TypeBool, usageUser, "")
	KeyExpertMode        = registerKey("expert-mode", TypeBool, usageUser, "")
	KeyFolge             = registerKey("folge", TypeIDSet, usageProperty, "")
	KeyFooterHTML        = registerKey("footer-html", TypeString, usageUser, "")
	KeyForward           = registerKey("forward", TypeIDSet, usageProperty, "")
	KeyHomeZettel        = registerKey("home-zettel", TypeID, usageUser, "")
	KeyLang              = registerKey("lang", TypeWord, usageUser, "")
	KeyLicense           = registerKey("license", TypeEmpty, usageUser, "")
	KeyListPageSize      = registerKey("list-page-size", TypeNumber, usageUser, "")

	KeyMarkerExternal    = registerKey("marker-external", TypeEmpty, usageUser, "")
	KeyModified          = registerKey("modified", TypeTimestamp, usageComputed, "")
	KeyPrecursor         = registerKey("precursor", TypeIDSet, usageUser, KeyFolge)
	KeyPublished         = registerKey("published", TypeTimestamp, usageProperty, "")
	KeyReadOnly          = registerKey("read-only", TypeWord, usageUser, "")
	KeySiteName          = registerKey("site-name", TypeString, usageUser, "")

	KeyURL               = registerKey("url", TypeURL, usageUser, "")
	KeyUserID            = registerKey("user-id", TypeWord, usageUser, "")
	KeyUserRole          = registerKey("user-role", TypeWord, usageUser, "")
	KeyVisibility        = registerKey("visibility", TypeWord, usageUser, "")
	KeyYAMLHeader        = registerKey("yaml-header", TypeBool, usageUser, "")
	KeyZettelFileSyntax  = registerKey("zettel-file-syntax", TypeWordSet, usageUser, "")
)

// Important values for some keys.
const (
	ValueRoleConfiguration = "configuration"
	ValueRoleUser          = "user"

	ValueRoleZettel        = "zettel"
	ValueSyntaxNone        = "none"
	ValueSyntaxZmk         = "zmk"
	ValueTrue              = "true"
	ValueFalse             = "false"
	ValueUserRoleReader    = "reader"
	ValueUserRoleWriter    = "writer"
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
	}
	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 string, def string) string {
	if value, ok := m.Get(key); ok {
		return value
	}
	return def
}

// Pairs 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) Pairs(allowComputed bool) []Pair {
	return m.doPairs(true, allowComputed)
}

// PairsRest returns all key/values pairs stored, except the values with
// predefined keys. The pairs are ordered by key.
func (m *Meta) PairsRest(allowComputed bool) []Pair {
	return m.doPairs(false, allowComputed)
}

func (m *Meta) doPairs(first bool, allowComputed bool) []Pair {
	result := make([]Pair, 0, len(m.pairs))
	if first {
		for _, key := range firstKeys {
			if value, ok := m.pairs[key]; ok {
				result = append(result, Pair{key, value})
			}
		}







|



















|







239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
	}
	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, ok := m.Get(key); ok {
		return value
	}
	return def
}

// Pairs 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) Pairs(allowComputed bool) []Pair {
	return m.doPairs(true, allowComputed)
}

// PairsRest returns all key/values pairs stored, except the values with
// predefined keys. The pairs are ordered by key.
func (m *Meta) PairsRest(allowComputed bool) []Pair {
	return m.doPairs(false, allowComputed)
}

func (m *Meta) doPairs(first, allowComputed bool) []Pair {
	result := make([]Pair, 0, len(m.pairs))
	if first {
		for _, key := range firstKeys {
			if value, ok := m.pairs[key]; ok {
				result = append(result, Pair{key, value})
			}
		}

Changes to domain/meta/parse.go.

177
178
179
180
181
182
183
184
185
186
187
188
189
			_, err := id.Parse(s)
			return err == nil
		})
	case TypeTimestamp:
		if _, ok := TimeValue(v); ok {
			m.Set(key, v)
		}
	case TypeEmpty:
		fallthrough
	default:
		addData(m, key, v)
	}
}







<
<




177
178
179
180
181
182
183


184
185
186
187
			_, err := id.Parse(s)
			return err == nil
		})
	case TypeTimestamp:
		if _, ok := TimeValue(v); ok {
			m.Set(key, v)
		}


	default:
		addData(m, key, v)
	}
}

Changes to domain/meta/type.go.

8
9
10
11
12
13
14

15
16
17
18
19
20
21
// under this license.
//-----------------------------------------------------------------------------

// Package meta provides the domain specific type 'meta'.
package meta

import (

	"strings"
	"time"
)

// DescriptionType is a description of a specific key type.
type DescriptionType struct {
	Name  string







>







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// under this license.
//-----------------------------------------------------------------------------

// Package meta provides the domain specific type 'meta'.
package meta

import (
	"strconv"
	"strings"
	"time"
)

// DescriptionType is a description of a specific key type.
type DescriptionType struct {
	Name  string
74
75
76
77
78
79
80








81
82
83
84
85
86
87
	if key != KeyID {
		for i, val := range values {
			values[i] = trimValue(val)
		}
		m.pairs[key] = strings.Join(values, " ")
	}
}









// SetNow stores the current timestamp under the given key.
func (m *Meta) SetNow(key string) {
	m.Set(key, time.Now().Format("20060102150405"))
}

// BoolValue returns the value interpreted as a bool.







>
>
>
>
>
>
>
>







75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
	if key != KeyID {
		for i, val := range values {
			values[i] = trimValue(val)
		}
		m.pairs[key] = strings.Join(values, " ")
	}
}

// CleanTag removes the number charachter ('#') from a tag value.
func CleanTag(tag string) string {
	if len(tag) > 1 && tag[0] == '#' {
		return tag[1:]
	}
	return tag
}

// SetNow stores the current timestamp under the given key.
func (m *Meta) SetNow(key string) {
	m.Set(key, time.Now().Format("20060102150405"))
}

// BoolValue returns the value interpreted as a bool.
129
130
131
132
133
134
135













136
137
138
139
140
141
142
143
144










func (m *Meta) GetList(key string) ([]string, bool) {
	value, ok := m.Get(key)
	if !ok {
		return nil, false
	}
	return ListFromValue(value), true
}














// GetListOrNil retrieves the string list value of a given key. If there was
// nothing stores, a nil list is returned.
func (m *Meta) GetListOrNil(key string) []string {
	if value, ok := m.GetList(key); ok {
		return value
	}
	return nil
}

















>
>
>
>
>
>
>
>
>
>
>
>
>









>
>
>
>
>
>
>
>
>
>
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
func (m *Meta) GetList(key string) ([]string, bool) {
	value, ok := m.Get(key)
	if !ok {
		return nil, false
	}
	return ListFromValue(value), true
}

// GetTags returns the list of tags as a string list. Each tag does not begin
// with the '#' character, in contrast to `GetList`.
func (m *Meta) GetTags(key string) ([]string, bool) {
	tags, ok := m.GetList(key)
	if !ok {
		return nil, false
	}
	for i, tag := range tags {
		tags[i] = CleanTag(tag)
	}
	return tags, len(tags) > 0
}

// GetListOrNil retrieves the string list value of a given key. If there was
// nothing stores, a nil list is returned.
func (m *Meta) GetListOrNil(key string) []string {
	if value, ok := m.GetList(key); ok {
		return value
	}
	return nil
}

// GetNumber retrieves the numeric value of a given key.
func (m *Meta) GetNumber(key string) (int, bool) {
	if value, ok := m.Get(key); ok {
		if num, err := strconv.Atoi(value); err == nil {
			return num, true
		}
	}
	return 0, false
}

Changes to encoder/encoder.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
	WriteContent(io.Writer, *ast.ZettelNode) (int, error)
	WriteBlocks(io.Writer, ast.BlockSlice) (int, error)
	WriteInlines(io.Writer, ast.InlineSlice) (int, error)
}

// Some errors to signal when encoder methods are not implemented.
var (
	ErrNoWriteZettel  = errors.New("Method WriteZettel is not implemented")
	ErrNoWriteMeta    = errors.New("Method WriteMeta is not implemented")
	ErrNoWriteContent = errors.New("Method WriteContent is not implemented")
	ErrNoWriteBlocks  = errors.New("Method WriteBlocks is not implemented")
	ErrNoWriteInlines = errors.New("Method WriteInlines is not implemented")
)

// Option allows to configure an encoder
type Option interface {
	Name() string
}








|
|
|
|
|







31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
	WriteContent(io.Writer, *ast.ZettelNode) (int, error)
	WriteBlocks(io.Writer, ast.BlockSlice) (int, error)
	WriteInlines(io.Writer, ast.InlineSlice) (int, error)
}

// Some errors to signal when encoder methods are not implemented.
var (
	ErrNoWriteZettel  = errors.New("method WriteZettel is not implemented")
	ErrNoWriteMeta    = errors.New("method WriteMeta is not implemented")
	ErrNoWriteContent = errors.New("method WriteContent is not implemented")
	ErrNoWriteBlocks  = errors.New("method WriteBlocks is not implemented")
	ErrNoWriteInlines = errors.New("method WriteInlines is not implemented")
)

// Option allows to configure an encoder
type Option interface {
	Name() string
}

Changes to encoder/htmlenc/block.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	"zettelstore.de/z/ast"
)

// VisitPara emits HTML code for a paragraph: <p>...</p>
func (v *visitor) VisitPara(pn *ast.ParaNode) {
	v.b.WriteString("<p>")
	v.acceptInlineSlice(pn.Inlines)
	v.b.WriteString("</p>\n")
}

// VisitVerbatim emits HTML code for verbatim lines.
func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) {
	switch vn.Code {
	case ast.VerbatimProg:
		oldVisible := v.visibleSpace







|







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	"zettelstore.de/z/ast"
)

// VisitPara emits HTML code for a paragraph: <p>...</p>
func (v *visitor) VisitPara(pn *ast.ParaNode) {
	v.b.WriteString("<p>")
	v.acceptInlineSlice(pn.Inlines)
	v.writeEndPara()
}

// VisitVerbatim emits HTML code for verbatim lines.
func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) {
	switch vn.Code {
	case ast.VerbatimProg:
		oldVisible := v.visibleSpace
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
			} else {
				v.b.WriteString("<p>")
				inPara = true
			}
			v.acceptInlineSlice(pn.Inlines)
		} else {
			if inPara {
				v.b.WriteString("</p>\n")
				inPara = false
			}
			v.acceptItemSlice(item)
		}
	}
	if inPara {
		v.b.WriteString("</p>\n")
	}
	v.b.WriteString("</blockquote>\n")
}

func getParaItem(its ast.ItemSlice) *ast.ParaNode {
	if len(its) != 1 {
		return nil







|






|







192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
			} else {
				v.b.WriteString("<p>")
				inPara = true
			}
			v.acceptInlineSlice(pn.Inlines)
		} else {
			if inPara {
				v.writeEndPara()
				inPara = false
			}
			v.acceptItemSlice(item)
		}
	}
	if inPara {
		v.writeEndPara()
	}
	v.b.WriteString("</blockquote>\n")
}

func getParaItem(its ast.ItemSlice) *ast.ParaNode {
	if len(its) != 1 {
		return nil
333
334
335
336
337
338
339




		v.b.WriteString("\" title=\"")
		v.writeQuotedEscaped(bn.Title)
		v.b.WriteString("\">\n")
	default:
		v.b.WriteStrings("<p class=\"error\">Unable to display BLOB with syntax '", bn.Syntax, "'.</p>\n")
	}
}











>
>
>
>
333
334
335
336
337
338
339
340
341
342
343
		v.b.WriteString("\" title=\"")
		v.writeQuotedEscaped(bn.Title)
		v.b.WriteString("\">\n")
	default:
		v.b.WriteStrings("<p class=\"error\">Unable to display BLOB with syntax '", bn.Syntax, "'.</p>\n")
	}
}

func (v *visitor) writeEndPara() {
	v.b.WriteString("</p>\n")
}

Changes to encoder/htmlenc/htmlenc.go.

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
		switch opt.Key {
		case "newwindow":
			he.newWindow = opt.Value
		case "xhtml":
			he.xhtml = opt.Value
		}
	case *encoder.StringsOption:
		switch opt.Key {
		case "no-meta":
			he.ignoreMeta = make(map[string]bool, len(opt.Value))
			for _, v := range opt.Value {
				he.ignoreMeta[v] = true
			}
		}
	case *encoder.AdaptLinkOption:
		he.adaptLink = opt.Adapter







<
|







52
53
54
55
56
57
58

59
60
61
62
63
64
65
66
		switch opt.Key {
		case "newwindow":
			he.newWindow = opt.Value
		case "xhtml":
			he.xhtml = opt.Value
		}
	case *encoder.StringsOption:

		if opt.Key == "no-meta" {
			he.ignoreMeta = make(map[string]bool, len(opt.Value))
			for _, v := range opt.Value {
				he.ignoreMeta[v] = true
			}
		}
	case *encoder.AdaptLinkOption:
		he.adaptLink = opt.Adapter

Changes to encoder/htmlenc/inline.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func (v *visitor) VisitText(tn *ast.TextNode) {
	v.writeHTMLEscaped(tn.Text)
}

// VisitTag writes tag content.
func (v *visitor) VisitTag(tn *ast.TagNode) {
	// TODO: erst mal als span. Link wäre gut, muss man vermutlich via Callback lösen.
	v.b.WriteString("<span class=\"zettel-tag\">")
	v.writeHTMLEscaped(tn.Tag)
	v.b.WriteString("</span>")
}

// VisitSpace emits a white space.
func (v *visitor) VisitSpace(sn *ast.SpaceNode) {
	if v.inVerse || v.xhtml {







|







23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func (v *visitor) VisitText(tn *ast.TextNode) {
	v.writeHTMLEscaped(tn.Text)
}

// VisitTag writes tag content.
func (v *visitor) VisitTag(tn *ast.TagNode) {
	// TODO: erst mal als span. Link wäre gut, muss man vermutlich via Callback lösen.
	v.b.WriteString("<span class=\"zettel-tag\">#")
	v.writeHTMLEscaped(tn.Tag)
	v.b.WriteString("</span>")
}

// VisitSpace emits a white space.
func (v *visitor) VisitSpace(sn *ast.SpaceNode) {
	if v.inVerse || v.xhtml {
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
			return
		}
	}
	v.lang.push(ln.Attrs)
	defer v.lang.pop()

	switch ln.Ref.State {
	case ast.RefStateZettelSelf, ast.RefStateZettelFound, ast.RefStateLocal:
		v.writeAHref(ln.Ref, ln.Attrs, ln.Inlines)
	case ast.RefStateZettelBroken:
		attrs := ln.Attrs.Clone()
		attrs = attrs.Set("class", "zs-broken")
		attrs = attrs.Set("title", "Zettel not found") // l10n
		v.writeAHref(ln.Ref, attrs, ln.Inlines)
	case ast.RefStateExternal:
		attrs := ln.Attrs.Clone()
		attrs = attrs.Set("class", "zs-external")







|

|







64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
			return
		}
	}
	v.lang.push(ln.Attrs)
	defer v.lang.pop()

	switch ln.Ref.State {
	case ast.RefStateSelf, ast.RefStateFound, ast.RefStateHosted, ast.RefStateBased:
		v.writeAHref(ln.Ref, ln.Attrs, ln.Inlines)
	case ast.RefStateBroken:
		attrs := ln.Attrs.Clone()
		attrs = attrs.Set("class", "zs-broken")
		attrs = attrs.Set("title", "Zettel not found") // l10n
		v.writeAHref(ln.Ref, attrs, ln.Inlines)
	case ast.RefStateExternal:
		attrs := ln.Attrs.Clone()
		attrs = attrs.Set("class", "zs-external")

Changes to encoder/htmlenc/visitor.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
45
46
47
48
49
50
51



52
53

54
55
56
57
58
59
60
61










62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
var mapMetaKey = map[string]string{
	meta.KeyCopyright: "copyright",
	meta.KeyLicense:   "license",
}

func (v *visitor) acceptMeta(m *meta.Meta, withTitle bool) {
	for i, pair := range m.Pairs(true) {



		if i == 0 { // "title" is number 0...
			if withTitle && !v.enc.ignoreMeta[pair.Key] {

				v.b.WriteStrings("<meta name=\"zs-", pair.Key, "\" content=\"")
				v.writeQuotedEscaped(pair.Value)
				v.b.WriteString("\">")
			}
			continue
		}
		if !v.enc.ignoreMeta[pair.Key] {
			if pair.Key == meta.KeyTags {










				v.b.WriteString("\n<meta name=\"keywords\" content=\"")
				for i, val := range meta.ListFromValue(pair.Value) {
					if i > 0 {
						v.b.WriteString(", ")
					}
					v.writeQuotedEscaped(strings.TrimPrefix(val, "#"))
				}
				v.b.WriteString("\">")
			} else if key, ok := mapMetaKey[pair.Key]; ok {
				v.writeMeta("", key, pair.Value)
			} else {
				v.writeMeta("zs-", pair.Key, pair.Value)
			}
		}
	}
}

func (v *visitor) writeMeta(prefix, key, value string) {
	v.b.WriteStrings("\n<meta name=\"", prefix, key, "\" content=\"")
	v.writeQuotedEscaped(value)
	v.b.WriteString("\">")
}







>
>
>

|
>






<
|
>
>
>
>
>
>
>
>
>
>
|
|
|
|
|
|
|
|
<
<
<
<
<
<
<







45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82







83
84
85
86
87
88
89
var mapMetaKey = map[string]string{
	meta.KeyCopyright: "copyright",
	meta.KeyLicense:   "license",
}

func (v *visitor) acceptMeta(m *meta.Meta, withTitle bool) {
	for i, pair := range m.Pairs(true) {
		if v.enc.ignoreMeta[pair.Key] {
			continue
		}
		if i == 0 { // "title" is number 0...
			if withTitle {
				// TODO: title value may contain zmk elements
				v.b.WriteStrings("<meta name=\"zs-", pair.Key, "\" content=\"")
				v.writeQuotedEscaped(pair.Value)
				v.b.WriteString("\">")
			}
			continue
		}

		if pair.Key == meta.KeyTags {
			v.writeTags(pair.Value)
		} else if key, ok := mapMetaKey[pair.Key]; ok {
			v.writeMeta("", key, pair.Value)
		} else {
			v.writeMeta("zs-", pair.Key, pair.Value)
		}
	}
}

func (v *visitor) writeTags(tags string) {
	v.b.WriteString("\n<meta name=\"keywords\" content=\"")
	for i, val := range meta.ListFromValue(tags) {
		if i > 0 {
			v.b.WriteString(", ")
		}
		v.writeQuotedEscaped(strings.TrimPrefix(val, "#"))
	}
	v.b.WriteString("\">")







}

func (v *visitor) writeMeta(prefix, key, value string) {
	v.b.WriteStrings("\n<meta name=\"", prefix, key, "\" content=\"")
	v.writeQuotedEscaped(value)
	v.b.WriteString("\">")
}

Changes to encoder/jsonenc/djsonenc.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
320
321
322
323
324
325
326
327
328
329
330

331
332
333
334
335
336
337
338
339
340
	} else {
		v.writeNodeStart("Soft")
	}
	v.b.WriteByte('}')
}

var mapRefState = map[ast.RefState]string{
	ast.RefStateInvalid:      "invalid",
	ast.RefStateZettel:       "zettel",
	ast.RefStateZettelSelf:   "self",
	ast.RefStateZettelFound:  "zettel",

	ast.RefStateZettelBroken: "broken",
	ast.RefStateLocal:        "local",
	ast.RefStateExternal:     "external",
}

// VisitLink writes JSON code for links.
func (v *detailVisitor) VisitLink(ln *ast.LinkNode) {
	if adapt := v.enc.adaptLink; adapt != nil {
		n := adapt(ln)
		var ok bool







|
|
|
|
>
|
|
|







320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
	} else {
		v.writeNodeStart("Soft")
	}
	v.b.WriteByte('}')
}

var mapRefState = map[ast.RefState]string{
	ast.RefStateInvalid:  "invalid",
	ast.RefStateZettel:   "zettel",
	ast.RefStateSelf:     "self",
	ast.RefStateFound:    "zettel",
	ast.RefStateBroken:   "broken",
	ast.RefStateHosted:   "local",
	ast.RefStateBased:    "based",
	ast.RefStateExternal: "external",
}

// VisitLink writes JSON code for links.
func (v *detailVisitor) VisitLink(ln *ast.LinkNode) {
	if adapt := v.enc.adaptLink; adapt != nil {
		n := adapt(ln)
		var ok bool
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600













			first = false
		} else {
			v.b.WriteString(",\"")
		}
		v.b.Write(Escape(p.Key))
		v.b.WriteString("\":")
		if m.Type(p.Key).IsSet {
			v.b.WriteByte('[')
			for i, val := range meta.ListFromValue(p.Value) {
				if i > 0 {
					v.b.WriteByte(',')
				}
				v.b.WriteByte('"')
				v.b.Write(Escape(val))
				v.b.WriteByte('"')
			}
			v.b.WriteByte(']')
		} else {
			v.b.WriteByte('"')
			v.b.Write(Escape(p.Value))
			v.b.WriteByte('"')
		}
	}
}




















<
|
<
<
<
<
<
<
<
<







>
>
>
>
>
>
>
>
>
>
>
>
>
578
579
580
581
582
583
584

585








586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
			first = false
		} else {
			v.b.WriteString(",\"")
		}
		v.b.Write(Escape(p.Key))
		v.b.WriteString("\":")
		if m.Type(p.Key).IsSet {

			v.writeSetValue(p.Value)








		} else {
			v.b.WriteByte('"')
			v.b.Write(Escape(p.Value))
			v.b.WriteByte('"')
		}
	}
}

func (v *detailVisitor) writeSetValue(value string) {
	v.b.WriteByte('[')
	for i, val := range meta.ListFromValue(value) {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.b.WriteByte('"')
		v.b.Write(Escape(val))
		v.b.WriteByte('"')
	}
	v.b.WriteByte(']')
}

Changes to encoder/jsonenc/jsonenc.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
	})
}

// jsonEncoder is just a stub. It is not implemented. The real implementation
// is in file web/adapter/json.go
type jsonEncoder struct{}

// SetOption sets an option for the encoder
func (je *jsonEncoder) SetOption(option encoder.Option) {}

// WriteZettel writes the encoded zettel to the writer.
func (je *jsonEncoder) WriteZettel(
	w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	return 0, encoder.ErrNoWriteZettel
}







|







27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
	})
}

// jsonEncoder is just a stub. It is not implemented. The real implementation
// is in file web/adapter/json.go
type jsonEncoder struct{}

// SetOption does nothing because this encoder does not recognize any option.
func (je *jsonEncoder) SetOption(option encoder.Option) {}

// WriteZettel writes the encoded zettel to the writer.
func (je *jsonEncoder) WriteZettel(
	w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	return 0, encoder.ErrNoWriteZettel
}

Changes to encoder/nativeenc/nativeenc.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
			first = false
		}
		v.level--
		v.b.WriteByte(']')
	}
}

func (v *visitor) writeMetaString(m *meta.Meta, key string, native string) {
	if val, ok := m.Get(key); ok && len(val) > 0 {
		v.b.WriteStrings("\n[", native, " \"", val, "\"]")
	}
}

func (v *visitor) writeMetaList(m *meta.Meta, key string, native string) {
	if vals, ok := m.GetList(key); ok && len(vals) > 0 {
		v.b.WriteStrings("\n[", native)
		for _, val := range vals {
			v.b.WriteByte(' ')
			v.b.WriteString(val)
		}
		v.b.WriteByte(']')







|





|







131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
			first = false
		}
		v.level--
		v.b.WriteByte(']')
	}
}

func (v *visitor) writeMetaString(m *meta.Meta, key, native string) {
	if val, ok := m.Get(key); ok && len(val) > 0 {
		v.b.WriteStrings("\n[", native, " \"", val, "\"]")
	}
}

func (v *visitor) writeMetaList(m *meta.Meta, key, native string) {
	if vals, ok := m.GetList(key); ok && len(vals) > 0 {
		v.b.WriteStrings("\n[", native)
		for _, val := range vals {
			v.b.WriteByte(' ')
			v.b.WriteString(val)
		}
		v.b.WriteByte(']')
378
379
380
381
382
383
384
385
386
387
388

389
390
391
392
393
394
395
396
397
398
		v.b.WriteString("Break")
	} else {
		v.b.WriteString("Space")
	}
}

var mapRefState = map[ast.RefState]string{
	ast.RefStateInvalid:      "INVALID",
	ast.RefStateZettel:       "ZETTEL",
	ast.RefStateZettelSelf:   "SELF",
	ast.RefStateZettelFound:  "ZETTEL",

	ast.RefStateZettelBroken: "BROKEN",
	ast.RefStateLocal:        "LOCAL",
	ast.RefStateExternal:     "EXTERNAL",
}

// VisitLink writes native code for links.
func (v *visitor) VisitLink(ln *ast.LinkNode) {
	if adapt := v.enc.adaptLink; adapt != nil {
		n := adapt(ln)
		var ok bool







|
|
|
|
>
|
|
|







378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
		v.b.WriteString("Break")
	} else {
		v.b.WriteString("Space")
	}
}

var mapRefState = map[ast.RefState]string{
	ast.RefStateInvalid:  "INVALID",
	ast.RefStateZettel:   "ZETTEL",
	ast.RefStateSelf:     "SELF",
	ast.RefStateFound:    "ZETTEL",
	ast.RefStateBroken:   "BROKEN",
	ast.RefStateHosted:   "LOCAL",
	ast.RefStateBased:    "BASED",
	ast.RefStateExternal: "EXTERNAL",
}

// VisitLink writes native code for links.
func (v *visitor) VisitLink(ln *ast.LinkNode) {
	if adapt := v.enc.adaptLink; adapt != nil {
		n := adapt(ln)
		var ok bool

Changes to encoder/rawenc/rawenc.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	encoder.Register("raw", encoder.Info{
		Create: func() encoder.Encoder { return &rawEncoder{} },
	})
}

type rawEncoder struct{}

// SetOption sets an option for the encoder
func (re *rawEncoder) SetOption(option encoder.Option) {}

// WriteZettel writes the encoded zettel to the writer.
func (re *rawEncoder) WriteZettel(
	w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	b := encoder.NewBufWriter(w)
	if inhMeta {







|







23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	encoder.Register("raw", encoder.Info{
		Create: func() encoder.Encoder { return &rawEncoder{} },
	})
}

type rawEncoder struct{}

// SetOption does nothing because this encoder does not recognize any option.
func (re *rawEncoder) SetOption(option encoder.Option) {}

// WriteZettel writes the encoded zettel to the writer.
func (re *rawEncoder) WriteZettel(
	w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	b := encoder.NewBufWriter(w)
	if inhMeta {

Changes to encoder/textenc/textenc.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	encoder.Register("text", encoder.Info{
		Create: func() encoder.Encoder { return &textEncoder{} },
	})
}

type textEncoder struct{}

// SetOption sets an option for this encoder
func (te *textEncoder) SetOption(option encoder.Option) {}

// WriteZettel does nothing.
func (te *textEncoder) WriteZettel(
	w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	v := newVisitor(w)
	if inhMeta {







|







23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	encoder.Register("text", encoder.Info{
		Create: func() encoder.Encoder { return &textEncoder{} },
	})
}

type textEncoder struct{}

// SetOption does nothing because this encoder does not recognize any option.
func (te *textEncoder) SetOption(option encoder.Option) {}

// WriteZettel does nothing.
func (te *textEncoder) WriteZettel(
	w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	v := newVisitor(w)
	if inhMeta {

Changes to encoder/zmkenc/zmkenc.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
	encoder.Register("zmk", encoder.Info{
		Create: func() encoder.Encoder { return &zmkEncoder{} },
	})
}

type zmkEncoder struct{}

// SetOption sets an option for this encoder.
func (ze *zmkEncoder) SetOption(option encoder.Option) {}

// WriteZettel writes the encoded zettel to the writer.
func (ze *zmkEncoder) WriteZettel(
	w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	v := newVisitor(w, ze)
	if inhMeta {







|







25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
	encoder.Register("zmk", encoder.Info{
		Create: func() encoder.Encoder { return &zmkEncoder{} },
	})
}

type zmkEncoder struct{}

// SetOption does nothing because this encoder does not recognize any option.
func (ze *zmkEncoder) SetOption(option encoder.Option) {}

// WriteZettel writes the encoded zettel to the writer.
func (ze *zmkEncoder) WriteZettel(
	w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
	v := newVisitor(w, ze)
	if inhMeta {

Changes to go.mod.

1
2
3
4
5
6
7
8
9
10
11
module zettelstore.de/z

go 1.15

require (
	github.com/fsnotify/fsnotify v1.4.9
	github.com/pascaldekloe/jwt v1.10.0
	github.com/yuin/goldmark v1.3.0
	golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
	golang.org/x/text v0.3.0
)







|



1
2
3
4
5
6
7
8
9
10
11
module zettelstore.de/z

go 1.15

require (
	github.com/fsnotify/fsnotify v1.4.9
	github.com/pascaldekloe/jwt v1.10.0
	github.com/yuin/goldmark v1.3.2
	golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
	golang.org/x/text v0.3.0
)

Changes to go.sum.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs=
github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A=
github.com/yuin/goldmark v1.3.0 h1:DRvEHivhJ1fQhZbpmttnonfC674RycyZGE/5IJzDKgg=
github.com/yuin/goldmark v1.3.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=




|
|










1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs=
github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A=
github.com/yuin/goldmark v1.3.2 h1:YjHC5TgyMmHpicTgEqDN0Q96Xo8K6tLXPnmNOHXCgs0=
github.com/yuin/goldmark v1.3.2/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

Changes to index/index.go.

45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
	_, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType)
	return ok
}

// Port contains all the used functions to access zettel to be indexed.
type Port interface {
	RegisterObserver(func(place.ChangeInfo))
	FetchZids(context.Context) (map[id.Zid]bool, error)
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)
	GetZettel(context.Context, id.Zid) (domain.Zettel, error)
}

// Indexer contains all the functions of an index.
type Indexer interface {
	Enricher







|







45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
	_, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType)
	return ok
}

// Port contains all the used functions to access zettel to be indexed.
type Port interface {
	RegisterObserver(func(place.ChangeInfo))
	FetchZids(context.Context) (id.Set, error)
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)
	GetZettel(context.Context, id.Zid) (domain.Zettel, error)
}

// Indexer contains all the functions of an index.
type Indexer interface {
	Enricher
87
88
89
90
91
92
93

94
95
96

97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

// Store all relevant zettel data. There may be multiple implementations, i.e.
// memory-based, file-based, based on SQLite, ...
type Store interface {
	Enricher

	// UpdateReferences for a specific zettel.

	UpdateReferences(context.Context, *ZettelIndex)

	// DeleteZettel removes index data for given zettel.

	DeleteZettel(context.Context, id.Zid)

	// ReadStats populates st with store statistics.
	ReadStats(st *StoreStats)

	// Write the content to a Writer
	Write(io.Writer)
}

// StoreStats records statistics about the store.
type StoreStats struct {
	// Zettel is the number of zettel managed by the indexer.
	Zettel int

	// Updates count the number of metadata updates.
	Updates uint64
}







>
|


>
|




|











87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

// Store all relevant zettel data. There may be multiple implementations, i.e.
// memory-based, file-based, based on SQLite, ...
type Store interface {
	Enricher

	// UpdateReferences for a specific zettel.
	// Returns set of zettel identifier that must also be checked for changes.
	UpdateReferences(context.Context, *ZettelIndex) id.Set

	// DeleteZettel removes index data for given zettel.
	// Returns set of zettel identifier that must also be checked for changes.
	DeleteZettel(context.Context, id.Zid) id.Set

	// ReadStats populates st with store statistics.
	ReadStats(st *StoreStats)

	// Write the content to a Writer.
	Write(io.Writer)
}

// StoreStats records statistics about the store.
type StoreStats struct {
	// Zettel is the number of zettel managed by the indexer.
	Zettel int

	// Updates count the number of metadata updates.
	Updates uint64
}

Changes to index/indexer/anteroom.go.

12
13
14
15
16
17
18









19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

58




59

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119


120
121
122
123
124

125
126
127
128
129
130
131


132
133
134
135
136





137
138
























139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package indexer

import (
	"sync"

	"zettelstore.de/z/domain/id"
)










type anteroom struct {
	next    *anteroom
	waiting map[id.Zid]bool
	curLoad int
	reload  bool
}

type anterooms struct {
	mx      sync.Mutex
	first   *anteroom
	last    *anteroom
	maxLoad int
}

func newAnterooms(maxLoad int) *anterooms {
	return &anterooms{maxLoad: maxLoad}
}

func (ar *anterooms) Enqueue(zid id.Zid, val bool) {
	if !zid.IsValid() {
		return
	}
	ar.mx.Lock()
	defer ar.mx.Unlock()
	if ar.first == nil {
		ar.first = ar.makeAnteroom(zid, val)
		ar.last = ar.first
		return
	}
	for room := ar.first; room != nil; room = room.next {
		if room.reload {
			continue // Do not place zettel in reload room
		}
		if v, ok := room.waiting[zid]; ok {
			if val == v {
				return
			}
			room.waiting[zid] = val

			return




		}

	}
	if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) {
		room.waiting[zid] = val
		room.curLoad++
		return
	}
	room := ar.makeAnteroom(zid, val)
	ar.last.next = room
	ar.last = room
}

func (ar *anterooms) makeAnteroom(zid id.Zid, val bool) *anteroom {
	cap := ar.maxLoad
	if cap == 0 {
		cap = 100
	}
	waiting := make(map[id.Zid]bool, cap)
	waiting[zid] = val
	return &anteroom{next: nil, waiting: waiting, curLoad: 1, reload: false}
}

func (ar *anterooms) Reset() {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	ar.first = ar.makeAnteroom(id.Invalid, true)
	ar.last = ar.first
}

func (ar *anterooms) Reload(delZids []id.Zid, newZids map[id.Zid]bool) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	delWaiting := make(map[id.Zid]bool, len(delZids))
	for _, zid := range delZids {
		if zid.IsValid() {
			delWaiting[zid] = false
		}
	}
	newWaiting := make(map[id.Zid]bool, len(newZids))
	for zid := range newZids {
		if zid.IsValid() {
			newWaiting[zid] = true
		}
	}

	// Delete previous reload rooms
	room := ar.first
	for ; room != nil && room.reload; room = room.next {
	}
	ar.first = room
	if room == nil {
		ar.last = nil
	}

	if ds := len(delWaiting); ds > 0 {
		if ns := len(newWaiting); ns > 0 {
			roomNew := &anteroom{next: ar.first, waiting: newWaiting, curLoad: ns, reload: true}
			ar.first = &anteroom{next: roomNew, waiting: delWaiting, curLoad: ds, reload: true}
			if roomNew.next == nil {
				ar.last = roomNew
			}


		} else {
			ar.first = &anteroom{next: ar.first, waiting: delWaiting, curLoad: ds}
			if ar.first.next == nil {
				ar.last = ar.first
			}

		}
	} else {
		if ns := len(newWaiting); ns > 0 {
			ar.first = &anteroom{next: ar.first, waiting: newWaiting, curLoad: ns}
			if ar.first.next == nil {
				ar.last = ar.first
			}


		} else {
			ar.first = nil
			ar.last = nil
		}
	}





}

























func (ar *anterooms) Dequeue() (id.Zid, bool) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	if ar.first == nil {
		return id.Invalid, false
	}
	for zid, val := range ar.first.waiting {
		delete(ar.first.waiting, zid)
		if len(ar.first.waiting) == 0 {
			ar.first = ar.first.next
			if ar.first == nil {
				ar.last = nil
			}
		}
		return zid, val
	}
	return id.Invalid, false
}







>
>
>
>
>
>
>
>
>



|















|
|





|







|
|
|
|
|
>

>
>
>
>

>


|



|




|
|
|
|

|
|






|



|


|
<
<
<
<
<
|
<
<
<
<
<
|
<
<
<
<
<
<
<
<








>
>
|
|
|
|
|
>
|
|
|
|
|
|
|
>
>
|
|
|
|
|
>
>
>
>
>
|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|



|

|







|

|

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106





107





108








109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package indexer

import (
	"sync"

	"zettelstore.de/z/domain/id"
)

type arAction int

const (
	arNothing arAction = iota
	arReload
	arUpdate
	arDelete
)

type anteroom struct {
	next    *anteroom
	waiting map[id.Zid]arAction
	curLoad int
	reload  bool
}

type anterooms struct {
	mx      sync.Mutex
	first   *anteroom
	last    *anteroom
	maxLoad int
}

func newAnterooms(maxLoad int) *anterooms {
	return &anterooms{maxLoad: maxLoad}
}

func (ar *anterooms) Enqueue(zid id.Zid, action arAction) {
	if !zid.IsValid() || action == arNothing || action == arReload {
		return
	}
	ar.mx.Lock()
	defer ar.mx.Unlock()
	if ar.first == nil {
		ar.first = ar.makeAnteroom(zid, action)
		ar.last = ar.first
		return
	}
	for room := ar.first; room != nil; room = room.next {
		if room.reload {
			continue // Do not place zettel in reload room
		}
		a, ok := room.waiting[zid]
		if !ok {
			continue
		}
		switch action {
		case a:
			return
		case arUpdate:
			room.waiting[zid] = action
		case arDelete:
			room.waiting[zid] = action
		}
		return
	}
	if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) {
		room.waiting[zid] = action
		room.curLoad++
		return
	}
	room := ar.makeAnteroom(zid, action)
	ar.last.next = room
	ar.last = room
}

func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom {
	c := ar.maxLoad
	if c == 0 {
		c = 100
	}
	waiting := make(map[id.Zid]arAction, c)
	waiting[zid] = action
	return &anteroom{next: nil, waiting: waiting, curLoad: 1, reload: false}
}

func (ar *anterooms) Reset() {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	ar.first = ar.makeAnteroom(id.Invalid, arReload)
	ar.last = ar.first
}

func (ar *anterooms) Reload(delZids id.Slice, newZids id.Set) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	delWaiting := createWaitingSlice(delZids, arDelete)





	newWaiting := createWaitingSet(newZids, arUpdate)





	ar.deleteReloadedRooms()









	if ds := len(delWaiting); ds > 0 {
		if ns := len(newWaiting); ns > 0 {
			roomNew := &anteroom{next: ar.first, waiting: newWaiting, curLoad: ns, reload: true}
			ar.first = &anteroom{next: roomNew, waiting: delWaiting, curLoad: ds, reload: true}
			if roomNew.next == nil {
				ar.last = roomNew
			}
			return
		}

		ar.first = &anteroom{next: ar.first, waiting: delWaiting, curLoad: ds}
		if ar.first.next == nil {
			ar.last = ar.first
		}
		return
	}

	if ns := len(newWaiting); ns > 0 {
		ar.first = &anteroom{next: ar.first, waiting: newWaiting, curLoad: ns}
		if ar.first.next == nil {
			ar.last = ar.first
		}
		return
	}

	ar.first = nil
	ar.last = nil
}

func createWaitingSlice(zids id.Slice, action arAction) map[id.Zid]arAction {
	waitingSet := make(map[id.Zid]arAction, len(zids))
	for _, zid := range zids {
		if zid.IsValid() {
			waitingSet[zid] = action
		}
	}
	return waitingSet
}

func createWaitingSet(zids id.Set, action arAction) map[id.Zid]arAction {
	waitingSet := make(map[id.Zid]arAction, len(zids))
	for zid := range zids {
		if zid.IsValid() {
			waitingSet[zid] = action
		}
	}
	return waitingSet
}

func (ar *anterooms) deleteReloadedRooms() {
	room := ar.first
	for room != nil && room.reload {
		room = room.next
	}
	ar.first = room
	if room == nil {
		ar.last = nil
	}
}

func (ar *anterooms) Dequeue() (arAction, id.Zid) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	if ar.first == nil {
		return arNothing, id.Invalid
	}
	for zid, action := range ar.first.waiting {
		delete(ar.first.waiting, zid)
		if len(ar.first.waiting) == 0 {
			ar.first = ar.first.next
			if ar.first == nil {
				ar.last = nil
			}
		}
		return action, zid
	}
	return arNothing, id.Invalid
}

Changes to index/indexer/anteroom_test.go.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
	"testing"

	"zettelstore.de/z/domain/id"
)

func TestSimple(t *testing.T) {
	ar := newAnterooms(2)
	ar.Enqueue(id.Zid(1), true)
	zid, val := ar.Dequeue()
	if zid != id.Zid(1) || val != true {
		t.Errorf("Expected 1/true, but got %v/%v", zid, val)
	}
	zid, val = ar.Dequeue()
	if zid != id.Invalid && val != false {
		t.Errorf("Expected invalid Zid, but got %v", zid)
	}
	ar.Enqueue(id.Zid(1), true)
	ar.Enqueue(id.Zid(2), true)
	if ar.first != ar.last {
		t.Errorf("Expected one room, but got more")
	}
	ar.Enqueue(id.Zid(3), true)
	if ar.first == ar.last {
		t.Errorf("Expected more than one room, but got only one")
	}

	count := 0
	for ; ; count++ {
		zid, val := ar.Dequeue()
		if zid == id.Invalid && val == false {
			break
		}
	}
	if count != 3 {
		t.Errorf("Expected 3 dequeues, but got %v", count)
	}
}

func TestReset(t *testing.T) {
	ar := newAnterooms(1)
	ar.Enqueue(id.Zid(1), true)
	ar.Reset()
	zid, val := ar.Dequeue()
	if zid != id.Invalid && val != true {
		t.Errorf("Expected invalid Zid, but got %v/%v", zid, val)
	}
	ar.Reload([]id.Zid{id.Zid(2)}, map[id.Zid]bool{id.Zid(3): true, id.Zid(4): false})
	ar.Enqueue(id.Zid(5), true)
	ar.Enqueue(id.Zid(5), false)
	ar.Enqueue(id.Zid(5), false)
	ar.Enqueue(id.Zid(5), true)
	if ar.first == ar.last || ar.first.next == ar.last || ar.first.next.next != ar.last {
		t.Errorf("Expected 3 rooms")
	}
	zid, val = ar.Dequeue()
	if zid != id.Zid(2) || val != false {
		t.Errorf("Expected 2/false, but got %v/%v", zid, val)
	}
	zid1, val := ar.Dequeue()
	if val != true {
		t.Errorf("Expected true, but got %v", val)
	}
	zid2, val := ar.Dequeue()
	if val != true {
		t.Errorf("Expected true, but got %v", val)
	}
	if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) {
		t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2)
	}
	zid, val = ar.Dequeue()
	if zid != id.Zid(5) || val != true {
		t.Errorf("Expected 5/true, but got %v/%v", zid, val)
	}
	zid, val = ar.Dequeue()
	if zid != id.Invalid && val != false {
		t.Errorf("Expected invalid Zid, but got %v", zid)
	}

	ar = newAnterooms(1)
	ar.Reload(nil, map[id.Zid]bool{id.Zid(6): true})
	zid, val = ar.Dequeue()
	if zid != id.Zid(6) || val != true {
		t.Errorf("Expected 6/true, but got %v/%v", zid, val)
	}
	zid, val = ar.Dequeue()
	if zid != id.Invalid && val != false {
		t.Errorf("Expected invalid Zid, but got %v", zid)
	}

	ar = newAnterooms(1)
	ar.Reload([]id.Zid{id.Zid(7)}, nil)
	zid, val = ar.Dequeue()
	if zid != id.Zid(7) || val != false {
		t.Errorf("Expected 7/false, but got %v/%v", zid, val)
	}
	zid, val = ar.Dequeue()
	if zid != id.Invalid && val != false {
		t.Errorf("Expected invalid Zid, but got %v", zid)
	}

	ar = newAnterooms(1)
	ar.Enqueue(id.Zid(8), true)
	ar.Reload(nil, nil)
	zid, val = ar.Dequeue()
	if zid != id.Invalid && val != false {
		t.Errorf("Expected invalid Zid, but got %v", zid)
	}
}







|
|
|
|

|
|


|
|



|





|
|
|










|

|
|
|

|
|
|
|
|



|
|
|

|
|
|

|
|
|




|
|
|

|
|
|



|
|
|
|

|
|
|



|
|
|
|

|
|
|



|

|
|
|


15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
	"testing"

	"zettelstore.de/z/domain/id"
)

func TestSimple(t *testing.T) {
	ar := newAnterooms(2)
	ar.Enqueue(id.Zid(1), arUpdate)
	action, zid := ar.Dequeue()
	if zid != id.Zid(1) || action != arUpdate {
		t.Errorf("Expected 1/arUpdate, but got %v/%v", zid, action)
	}
	action, zid = ar.Dequeue()
	if zid != id.Invalid && action != arDelete {
		t.Errorf("Expected invalid Zid, but got %v", zid)
	}
	ar.Enqueue(id.Zid(1), arUpdate)
	ar.Enqueue(id.Zid(2), arUpdate)
	if ar.first != ar.last {
		t.Errorf("Expected one room, but got more")
	}
	ar.Enqueue(id.Zid(3), arUpdate)
	if ar.first == ar.last {
		t.Errorf("Expected more than one room, but got only one")
	}

	count := 0
	for ; count < 1000; count++ {
		action, _ := ar.Dequeue()
		if action == arNothing {
			break
		}
	}
	if count != 3 {
		t.Errorf("Expected 3 dequeues, but got %v", count)
	}
}

func TestReset(t *testing.T) {
	ar := newAnterooms(1)
	ar.Enqueue(id.Zid(1), arUpdate)
	ar.Reset()
	action, zid := ar.Dequeue()
	if action != arReload || zid != id.Invalid {
		t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid)
	}
	ar.Reload(id.Slice{2}, id.NewSet(3, 4))
	ar.Enqueue(id.Zid(5), arUpdate)
	ar.Enqueue(id.Zid(5), arDelete)
	ar.Enqueue(id.Zid(5), arDelete)
	ar.Enqueue(id.Zid(5), arUpdate)
	if ar.first == ar.last || ar.first.next == ar.last || ar.first.next.next != ar.last {
		t.Errorf("Expected 3 rooms")
	}
	action, zid = ar.Dequeue()
	if zid != id.Zid(2) || action != arDelete {
		t.Errorf("Expected 2/arDelete, but got %v/%v", zid, action)
	}
	action, zid1 := ar.Dequeue()
	if action != arUpdate {
		t.Errorf("Expected arUpdate, but got %v", action)
	}
	action, zid2 := ar.Dequeue()
	if action != arUpdate {
		t.Errorf("Expected arUpdate, but got %v", action)
	}
	if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) {
		t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2)
	}
	action, zid = ar.Dequeue()
	if zid != id.Zid(5) || action != arUpdate {
		t.Errorf("Expected 5/arUpdate, but got %v/%v", zid, action)
	}
	action, zid = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}

	ar = newAnterooms(1)
	ar.Reload(nil, id.NewSet(id.Zid(6)))
	action, zid = ar.Dequeue()
	if zid != id.Zid(6) || action != arUpdate {
		t.Errorf("Expected 6/arUpdate, but got %v/%v", zid, action)
	}
	action, zid = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}

	ar = newAnterooms(1)
	ar.Reload(id.Slice{7}, nil)
	action, zid = ar.Dequeue()
	if zid != id.Zid(7) || action != arDelete {
		t.Errorf("Expected 7/arDelete, but got %v/%v", zid, action)
	}
	action, zid = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}

	ar = newAnterooms(1)
	ar.Enqueue(id.Zid(8), arUpdate)
	ar.Reload(nil, nil)
	action, zid = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}
}

Changes to index/indexer/indexer.go.

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
}

func (idx *indexer) observer(ci place.ChangeInfo) {
	switch ci.Reason {
	case place.OnReload:
		idx.ar.Reset()
	case place.OnUpdate:
		idx.ar.Enqueue(ci.Zid, true)
	case place.OnDelete:
		idx.ar.Enqueue(ci.Zid, false)
	default:
		return
	}
	select {
	case idx.ready <- struct{}{}:
	default:
	}







|

|







52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
}

func (idx *indexer) observer(ci place.ChangeInfo) {
	switch ci.Reason {
	case place.OnReload:
		idx.ar.Reset()
	case place.OnUpdate:
		idx.ar.Enqueue(ci.Zid, arUpdate)
	case place.OnDelete:
		idx.ar.Enqueue(ci.Zid, arDelete)
	default:
		return
	}
	select {
	case idx.ready <- struct{}{}:
	default:
	}
107
108
109
110
111
112
113
114
115
116
117
118

119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143


144
145
146
147
148
149
150
151
152
153

154



155
156

157
158
159
160
161
162
163
164









165


166
167
168
169

170
171



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190

191
192
193
194
195
196
197
	st.DurLastIndex = idx.durLastIndex
	idx.mx.RUnlock()
	idx.store.ReadStats(&st.Store)
}

type indexerPort interface {
	getMetaPort
	FetchZids(ctx context.Context) (map[id.Zid]bool, error)
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
}

// indexer runs in the background and updates the index data structures.

func (idx *indexer) indexer(p indexerPort) {
	// Something may panic. Ensure a running indexer.
	defer func() {
		if err := recover(); err != nil {
			go idx.indexer(p)
		}
	}()

	timerDuration := 15 * time.Second
	timer := time.NewTimer(timerDuration)
	ctx := index.NoEnrichContext(context.Background())
	for {
		start := time.Now()
		changed := false
		for {
			zid, val := idx.ar.Dequeue()
			if zid.IsValid() {
				changed = true
				idx.mx.Lock()
				idx.sinceReload++
				idx.mx.Unlock()
				if !val {
					idx.deleteZettel(zid)
					continue
				}



				zettel, err := p.GetZettel(ctx, zid)
				if err != nil {
					// TODO: on some errors put the zid into a "try later" set
					continue
				}
				idx.updateZettel(ctx, zettel, p)
				continue
			}


			if val == false {



				break
			}

			zids, err := p.FetchZids(ctx)
			if err == nil {
				idx.ar.Reload(nil, zids)
				idx.mx.Lock()
				idx.lastReload = time.Now()
				idx.sinceReload = 0
				idx.mx.Unlock()
			}









		}


		if changed {
			idx.mx.Lock()
			idx.durLastIndex = time.Now().Sub(start)
			idx.mx.Unlock()

		}




		select {
		case _, ok := <-idx.ready:
			if !ok {
				return
			}
		case _, ok := <-timer.C:
			if !ok {
				return
			}
			timer.Reset(timerDuration)
		case _, ok := <-idx.done:
			if !ok {
				if !timer.Stop() {
					<-timer.C
				}
				return
			}
		}
	}

}

type getMetaPort interface {
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
}

func (idx *indexer) updateZettel(ctx context.Context, zettel domain.Zettel, p getMetaPort) {







|




>













<
<
|
<
<
|
|
|
<
<
<
|
>
>
|
<
<
<
<
|
<
<
|

>
|
>
>
>
|
<
>








>
>
>
>
>
>
>
>
>
|
>
>
|

|

>

|
>
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<
>







107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132


133


134
135
136



137
138
139
140




141


142
143
144
145
146
147
148
149

150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198

199
200
201
202
203
204
205
206
	st.DurLastIndex = idx.durLastIndex
	idx.mx.RUnlock()
	idx.store.ReadStats(&st.Store)
}

type indexerPort interface {
	getMetaPort
	FetchZids(ctx context.Context) (id.Set, error)
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
}

// indexer runs in the background and updates the index data structures.
// This is the main service of the indexer.
func (idx *indexer) indexer(p indexerPort) {
	// Something may panic. Ensure a running indexer.
	defer func() {
		if err := recover(); err != nil {
			go idx.indexer(p)
		}
	}()

	timerDuration := 15 * time.Second
	timer := time.NewTimer(timerDuration)
	ctx := index.NoEnrichContext(context.Background())
	for {
		start := time.Now()


		if idx.workService(ctx, p) {


			idx.mx.Lock()
			idx.durLastIndex = time.Since(start)
			idx.mx.Unlock()



		}
		if !idx.sleepService(timer, timerDuration) {
			return
		}




	}


}

func (idx *indexer) workService(ctx context.Context, p indexerPort) bool {
	changed := false
	for {
		switch action, zid := idx.ar.Dequeue(); action {
		case arNothing:
			return changed

		case arReload:
			zids, err := p.FetchZids(ctx)
			if err == nil {
				idx.ar.Reload(nil, zids)
				idx.mx.Lock()
				idx.lastReload = time.Now()
				idx.sinceReload = 0
				idx.mx.Unlock()
			}
		case arUpdate:
			changed = true
			idx.mx.Lock()
			idx.sinceReload++
			idx.mx.Unlock()
			zettel, err := p.GetZettel(ctx, zid)
			if err != nil {
				// TODO: on some errors put the zid into a "try later" set
				continue
			}
			idx.updateZettel(ctx, zettel, p)
		case arDelete:
			changed = true
			idx.mx.Lock()
			idx.sinceReload++
			idx.mx.Unlock()
			idx.deleteZettel(zid)
		}
	}
}

func (idx *indexer) sleepService(timer *time.Timer, timerDuration time.Duration) bool {
	select {
	case _, ok := <-idx.ready:
		if !ok {
			return false
		}
	case _, ok := <-timer.C:
		if !ok {
			return false
		}
		timer.Reset(timerDuration)
	case _, ok := <-idx.done:
		if !ok {
			if !timer.Stop() {
				<-timer.C
			}
			return false
		}
	}

	return true
}

type getMetaPort interface {
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
}

func (idx *indexer) updateZettel(ctx context.Context, zettel domain.Zettel, p getMetaPort) {
211
212
213
214
215
216
217
218

219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260

261






			}
		}
	}
	zn := parser.ParseZettel(zettel, "")
	refs := collect.References(zn)
	updateReferences(ctx, refs.Links, p, zi)
	updateReferences(ctx, refs.Images, p, zi)
	idx.store.UpdateReferences(ctx, zi)

}

func updateValue(
	ctx context.Context, inverse string, value string, p getMetaPort, zi *index.ZettelIndex) {
	zid, err := id.Parse(value)
	if err != nil {
		return
	}
	if _, err := p.GetMeta(ctx, zid); err != nil {
		zi.AddDeadRef(zid)
		return
	}
	if inverse == "" {
		zi.AddBackRef(zid)
		return
	}
	zi.AddMetaRef(inverse, zid)
}

func updateReferences(
	ctx context.Context, refs []*ast.Reference, p getMetaPort, zi *index.ZettelIndex) {
	zrefs, _, _ := collect.DivideReferences(refs, false)
	for _, ref := range zrefs {
		updateReference(ctx, ref.Value, p, zi)
	}
}

func updateReference(
	ctx context.Context, value string, p getMetaPort, zi *index.ZettelIndex) {
	zid, err := id.Parse(value)
	if err != nil {
		return
	}
	if _, err := p.GetMeta(ctx, zid); err != nil {
		zi.AddDeadRef(zid)
		return
	}
	zi.AddBackRef(zid)
}

func (idx *indexer) deleteZettel(zid id.Zid) {
	idx.store.DeleteZettel(context.Background(), zid)

}













|
>


<
|















<
|


|



<
|












|
>

>
>
>
>
>
>
220
221
222
223
224
225
226
227
228
229
230

231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246

247
248
249
250
251
252
253

254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
			}
		}
	}
	zn := parser.ParseZettel(zettel, "")
	refs := collect.References(zn)
	updateReferences(ctx, refs.Links, p, zi)
	updateReferences(ctx, refs.Images, p, zi)
	toCheck := idx.store.UpdateReferences(ctx, zi)
	idx.checkZettel(toCheck)
}


func updateValue(ctx context.Context, inverse string, value string, p getMetaPort, zi *index.ZettelIndex) {
	zid, err := id.Parse(value)
	if err != nil {
		return
	}
	if _, err := p.GetMeta(ctx, zid); err != nil {
		zi.AddDeadRef(zid)
		return
	}
	if inverse == "" {
		zi.AddBackRef(zid)
		return
	}
	zi.AddMetaRef(inverse, zid)
}


func updateReferences(ctx context.Context, refs []*ast.Reference, p getMetaPort, zi *index.ZettelIndex) {
	zrefs, _, _ := collect.DivideReferences(refs, false)
	for _, ref := range zrefs {
		updateReference(ctx, ref.URL.Path, p, zi)
	}
}


func updateReference(ctx context.Context, value string, p getMetaPort, zi *index.ZettelIndex) {
	zid, err := id.Parse(value)
	if err != nil {
		return
	}
	if _, err := p.GetMeta(ctx, zid); err != nil {
		zi.AddDeadRef(zid)
		return
	}
	zi.AddBackRef(zid)
}

func (idx *indexer) deleteZettel(zid id.Zid) {
	toCheck := idx.store.DeleteZettel(context.Background(), zid)
	idx.checkZettel(toCheck)
}

func (idx *indexer) checkZettel(s id.Set) {
	for zid := range s {
		idx.ar.Enqueue(zid, arUpdate)
	}
}

Changes to index/memstore/memstore.go.

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

47
48
49
50
51
52
53
54
55

56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98


















99
100
101
102
103
104
105
106
107
108




109

110



111


112
113
114


115


116
117
118
119


120
121
122







123
124
125
126



127

128


129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164

165
166
167
168
169
170
171
172
173
174
175
176
177
178











179
180
181

















182
183
184
185
186
187
188
189


190
191
192
193
194
195
196
197
198
199
200
201
202
203
204


205



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229


230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253



254



255


256
257
258
259
260
261
262
263

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/index"
)

type metaRefs struct {
	forward  []id.Zid
	backward []id.Zid
}

type zettelIndex struct {
	dead     string
	forward  []id.Zid
	backward []id.Zid
	meta     map[string]metaRefs
}

func (zi *zettelIndex) isEmpty() bool {
	if len(zi.forward) > 0 || len(zi.backward) > 0 || zi.dead != "" {
		return false
	}
	return zi.meta == nil || len(zi.meta) == 0
}

type memStore struct {
	mx  sync.RWMutex
	idx map[id.Zid]*zettelIndex


	// Stats
	updates uint64
}

// New returns a new memory-based index store.
func New() index.Store {
	return &memStore{
		idx: make(map[id.Zid]*zettelIndex),

	}
}

func (ms *memStore) Enrich(ctx context.Context, m *meta.Meta) {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	zi, ok := ms.idx[m.Zid]
	if !ok {
		return
	}
	var updated bool
	if zi.dead != "" {
		m.Set(meta.KeyDead, zi.dead)
		updated = true
	}
	back := zi.backward
	if len(zi.backward) > 0 {
		m.Set(meta.KeyBackward, refsToString(zi.backward))
		updated = true
	}
	if len(zi.forward) > 0 {
		m.Set(meta.KeyForward, refsToString(zi.forward))
		back = remRefs(back, zi.forward)
		updated = true
	}
	if len(zi.meta) > 0 {
		for k, refs := range zi.meta {
			if len(refs.backward) > 0 {
				m.Set(k, refsToString(refs.backward))
				back = remRefs(back, refs.backward)
				updated = true
			}
		}
	}
	if len(back) > 0 {
		m.Set(meta.KeyBack, refsToString(back))
		updated = true
	}
	if updated {
		ms.updates++
	}
}



















func (ms *memStore) UpdateReferences(ctx context.Context, zidx *index.ZettelIndex) {
	ms.mx.Lock()
	defer ms.mx.Unlock()
	zi, ziExist := ms.idx[zidx.Zid]
	if !ziExist || zi == nil {
		zi = &zettelIndex{}
		ziExist = false
	}

	// Update dead references




	if drefs := zidx.GetDeadRefs(); len(drefs) > 0 {

		zi.dead = refsToString(drefs)



	} else {


		zi.dead = ""
	}



	// Update forward and backward references


	brefs := zidx.GetBackRefs()
	newRefs, remRefs := refsDiff(brefs, zi.forward)
	zi.forward = brefs
	for _, ref := range newRefs {


		bzi := ms.getEntry(ref)
		bzi.backward = addRef(bzi.backward, zidx.Zid)
	}







	for _, ref := range remRefs {
		bzi := ms.getEntry(ref)
		bzi.backward = remRef(bzi.backward, zidx.Zid)
	}





	// Update metadata references


	metarefs := zidx.GetMetaRefs()
	for key, mr := range zi.meta {
		if _, ok := metarefs[key]; ok {
			continue
		}
		ms.removeInverseMeta(zidx.Zid, key, mr.forward)
	}

	if zi.meta == nil {
		zi.meta = make(map[string]metaRefs)
	}
	for key, mrefs := range metarefs {
		mr := zi.meta[key]
		newRefs, remRefs := refsDiff(mrefs, mr.forward)
		mr.forward = mrefs
		zi.meta[key] = mr

		for _, ref := range newRefs {
			bzi := ms.getEntry(ref)
			if bzi.meta == nil {
				bzi.meta = make(map[string]metaRefs)
			}
			bmr := bzi.meta[key]
			bmr.backward = addRef(bmr.backward, zidx.Zid)
			bzi.meta[key] = bmr
		}
		ms.removeInverseMeta(zidx.Zid, key, remRefs)
	}

	// Check if zi must be inserted into ms.idx
	if !ziExist && !zi.isEmpty() {
		ms.idx[zidx.Zid] = zi
	}
}

func (ms *memStore) getEntry(zid id.Zid) *zettelIndex {

	if zi, ok := ms.idx[zid]; ok {
		return zi
	}
	zi := &zettelIndex{}
	ms.idx[zid] = zi
	return zi
}

func (ms *memStore) DeleteZettel(ctx context.Context, zid id.Zid) {
	ms.mx.Lock()
	defer ms.mx.Unlock()

	zi, ok := ms.idx[zid]
	if !ok {











		return
	}


















	for _, ref := range zi.forward {
		if fzi, ok := ms.idx[ref]; ok {
			fzi.backward = remRef(fzi.backward, zid)
		}
	}
	for _, ref := range zi.backward {
		if bzi, ok := ms.idx[ref]; ok {
			bzi.forward = remRef(bzi.forward, zid)


		}
	}
	if len(zi.meta) > 0 {
		for key, mrefs := range zi.meta {
			ms.removeInverseMeta(zid, key, mrefs.forward)
		}
	}
	delete(ms.idx, zid)
}

func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward []id.Zid) {
	// Must only be called if ms.mx is write-locked!
	for _, ref := range forward {
		if bzi, ok := ms.idx[ref]; ok {
			if bzi.meta != nil {


				if bmr, ok := bzi.meta[key]; ok {



					bmr.backward = remRef(bmr.backward, zid)
					if len(bmr.backward) > 0 || len(bmr.forward) > 0 {
						bzi.meta[key] = bmr
					} else {
						delete(bzi.meta, key)
						if len(bzi.meta) == 0 {
							bzi.meta = nil
						}
					}
				}
			}
		}
	}
}

func (ms *memStore) ReadStats(st *index.StoreStats) {
	ms.mx.RLock()
	st.Zettel = len(ms.idx)
	st.Updates = ms.updates
	ms.mx.RUnlock()
}

func (ms *memStore) Write(w io.Writer) {
	ms.mx.RLock()


	zids := make([]id.Zid, 0, len(ms.idx))
	for id := range ms.idx {
		zids = append(zids, id)
	}
	id.Sort(zids)
	for _, id := range zids {
		fmt.Fprintln(w, id)
		zi := ms.idx[id]
		fmt.Fprintln(w, "-", zi.dead)
		writeZidsLn(w, ">", zi.forward)
		writeZidsLn(w, "<", zi.backward)
		if zi.meta == nil {
			fmt.Fprintln(w, "*NIL")
		} else if len(zi.meta) == 0 {
			fmt.Fprintln(w, "*(0)")
		} else {
			for k, fb := range zi.meta {
				fmt.Fprintln(w, "*", k)
				writeZidsLn(w, "]", fb.forward)
				writeZidsLn(w, "[", fb.backward)
			}
		}
	}
	ms.mx.RUnlock()



}






func writeZidsLn(w io.Writer, prefix string, zids []id.Zid) {
	io.WriteString(w, prefix)
	for _, zid := range zids {
		io.WriteString(w, " ")
		w.Write(zid.Bytes())
	}
	fmt.Fprintln(w)
}







|
|



|
|
|




|






|
|
>








|
>











|
|


|

|



|






|






|







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|








|
>
>
>
>
|
>
|
>
>
>
|
>
>
|


>
>
|
>
>
|
|
|
|
>
>
|
|

>
>
>
>
>
>
>




>
>
>
|
>
|
>
>







<




















<
<
<
<
<



>








|





>
>
>
>
>
>
>
>
>
>
>
|
|

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>








>
>
|
<
<
|
<


|


|


|
|
>
>
|
>
>
>
|
|
|
|
|
|
|
<
<
<














>
>
|



|


















|
>
>
>
|
>
>
>
|
>
>
|







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184

185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204





205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264


265

266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/index"
)

type metaRefs struct {
	forward  id.Slice
	backward id.Slice
}

type zettelIndex struct {
	dead     id.Slice
	forward  id.Slice
	backward id.Slice
	meta     map[string]metaRefs
}

func (zi *zettelIndex) isEmpty() bool {
	if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 {
		return false
	}
	return zi.meta == nil || len(zi.meta) == 0
}

type memStore struct {
	mx   sync.RWMutex
	idx  map[id.Zid]*zettelIndex
	dead map[id.Zid]id.Slice // map dead refs where they occur

	// Stats
	updates uint64
}

// New returns a new memory-based index store.
func New() index.Store {
	return &memStore{
		idx:  make(map[id.Zid]*zettelIndex),
		dead: make(map[id.Zid]id.Slice),
	}
}

func (ms *memStore) Enrich(ctx context.Context, m *meta.Meta) {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	zi, ok := ms.idx[m.Zid]
	if !ok {
		return
	}
	var updated bool
	if len(zi.dead) > 0 {
		m.Set(meta.KeyDead, zi.dead.String())
		updated = true
	}
	back := removeOtherMetaRefs(m, zi.backward.Copy())
	if len(zi.backward) > 0 {
		m.Set(meta.KeyBackward, zi.backward.String())
		updated = true
	}
	if len(zi.forward) > 0 {
		m.Set(meta.KeyForward, zi.forward.String())
		back = remRefs(back, zi.forward)
		updated = true
	}
	if len(zi.meta) > 0 {
		for k, refs := range zi.meta {
			if len(refs.backward) > 0 {
				m.Set(k, refs.backward.String())
				back = remRefs(back, refs.backward)
				updated = true
			}
		}
	}
	if len(back) > 0 {
		m.Set(meta.KeyBack, back.String())
		updated = true
	}
	if updated {
		ms.updates++
	}
}

func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice {
	for _, p := range m.PairsRest(false) {
		switch meta.Type(p.Key) {
		case meta.TypeID:
			if zid, err := id.Parse(p.Value); err == nil {
				back = remRef(back, zid)
			}
		case meta.TypeIDSet:
			for _, val := range meta.ListFromValue(p.Value) {
				if zid, err := id.Parse(val); err == nil {
					back = remRef(back, zid)
				}
			}
		}
	}
	return back
}

func (ms *memStore) UpdateReferences(ctx context.Context, zidx *index.ZettelIndex) id.Set {
	ms.mx.Lock()
	defer ms.mx.Unlock()
	zi, ziExist := ms.idx[zidx.Zid]
	if !ziExist || zi == nil {
		zi = &zettelIndex{}
		ziExist = false
	}

	// Is this zettel an old dead reference mentioned in other zettel?
	var toCheck id.Set
	if refs, ok := ms.dead[zidx.Zid]; ok {
		// These must be checked later again
		toCheck = id.NewSet(refs...)
		delete(ms.dead, zidx.Zid)
	}

	ms.updateDeadReferences(zidx, zi)
	ms.updateForwardBackwardReferences(zidx, zi)
	ms.updateMetadataReferences(zidx, zi)

	// Check if zi must be inserted into ms.idx
	if !ziExist && !zi.isEmpty() {
		ms.idx[zidx.Zid] = zi
	}

	return toCheck
}

func (ms *memStore) updateDeadReferences(zidx *index.ZettelIndex, zi *zettelIndex) {
	// Must only be called if ms.mx is write-locked!
	drefs := zidx.GetDeadRefs()
	newRefs, remRefs := refsDiff(drefs, zi.dead)
	zi.dead = drefs
	for _, ref := range remRefs {
		ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid)
	}
	for _, ref := range newRefs {
		ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid)
	}
}

func (ms *memStore) updateForwardBackwardReferences(zidx *index.ZettelIndex, zi *zettelIndex) {
	// Must only be called if ms.mx is write-locked!
	brefs := zidx.GetBackRefs()
	newRefs, remRefs := refsDiff(brefs, zi.forward)
	zi.forward = brefs
	for _, ref := range remRefs {
		bzi := ms.getEntry(ref)
		bzi.backward = remRef(bzi.backward, zidx.Zid)
	}
	for _, ref := range newRefs {
		bzi := ms.getEntry(ref)
		bzi.backward = addRef(bzi.backward, zidx.Zid)
	}
}

func (ms *memStore) updateMetadataReferences(zidx *index.ZettelIndex, zi *zettelIndex) {
	// Must only be called if ms.mx is write-locked!
	metarefs := zidx.GetMetaRefs()
	for key, mr := range zi.meta {
		if _, ok := metarefs[key]; ok {
			continue
		}
		ms.removeInverseMeta(zidx.Zid, key, mr.forward)
	}

	if zi.meta == nil {
		zi.meta = make(map[string]metaRefs)
	}
	for key, mrefs := range metarefs {
		mr := zi.meta[key]
		newRefs, remRefs := refsDiff(mrefs, mr.forward)
		mr.forward = mrefs
		zi.meta[key] = mr

		for _, ref := range newRefs {
			bzi := ms.getEntry(ref)
			if bzi.meta == nil {
				bzi.meta = make(map[string]metaRefs)
			}
			bmr := bzi.meta[key]
			bmr.backward = addRef(bmr.backward, zidx.Zid)
			bzi.meta[key] = bmr
		}
		ms.removeInverseMeta(zidx.Zid, key, remRefs)
	}





}

func (ms *memStore) getEntry(zid id.Zid) *zettelIndex {
	// Must only be called if ms.mx is write-locked!
	if zi, ok := ms.idx[zid]; ok {
		return zi
	}
	zi := &zettelIndex{}
	ms.idx[zid] = zi
	return zi
}

func (ms *memStore) DeleteZettel(ctx context.Context, zid id.Zid) id.Set {
	ms.mx.Lock()
	defer ms.mx.Unlock()

	zi, ok := ms.idx[zid]
	if !ok {
		return nil
	}

	ms.deleteDeadSources(zid, zi)
	toCheck := ms.deleteForwardBackward(zid, zi)
	if len(zi.meta) > 0 {
		for key, mrefs := range zi.meta {
			ms.removeInverseMeta(zid, key, mrefs.forward)
		}
	}
	delete(ms.idx, zid)
	return toCheck
}

func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelIndex) {
	// Must only be called if ms.mx is write-locked!
	for _, ref := range zi.dead {
		if drefs, ok := ms.dead[ref]; ok {
			drefs = remRef(drefs, zid)
			if len(drefs) > 0 {
				ms.dead[ref] = drefs
			} else {
				delete(ms.dead, ref)
			}
		}
	}
}

func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) id.Set {
	// Must only be called if ms.mx is write-locked!
	var toCheck id.Set
	for _, ref := range zi.forward {
		if fzi, ok := ms.idx[ref]; ok {
			fzi.backward = remRef(fzi.backward, zid)
		}
	}
	for _, ref := range zi.backward {
		if bzi, ok := ms.idx[ref]; ok {
			bzi.forward = remRef(bzi.forward, zid)
			if toCheck == nil {
				toCheck = id.NewSet()
			}


			toCheck[ref] = true

		}
	}
	return toCheck
}

func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) {
	// Must only be called if ms.mx is write-locked!
	for _, ref := range forward {
		bzi, ok := ms.idx[ref]
		if !ok || bzi.meta == nil {
			continue
		}
		bmr, ok := bzi.meta[key]
		if !ok {
			continue
		}
		bmr.backward = remRef(bmr.backward, zid)
		if len(bmr.backward) > 0 || len(bmr.forward) > 0 {
			bzi.meta[key] = bmr
		} else {
			delete(bzi.meta, key)
			if len(bzi.meta) == 0 {
				bzi.meta = nil



			}
		}
	}
}

func (ms *memStore) ReadStats(st *index.StoreStats) {
	ms.mx.RLock()
	st.Zettel = len(ms.idx)
	st.Updates = ms.updates
	ms.mx.RUnlock()
}

func (ms *memStore) Write(w io.Writer) {
	ms.mx.RLock()
	defer ms.mx.RUnlock()

	zids := make(id.Slice, 0, len(ms.idx))
	for id := range ms.idx {
		zids = append(zids, id)
	}
	zids.Sort()
	for _, id := range zids {
		fmt.Fprintln(w, id)
		zi := ms.idx[id]
		fmt.Fprintln(w, "-", zi.dead)
		writeZidsLn(w, ">", zi.forward)
		writeZidsLn(w, "<", zi.backward)
		if zi.meta == nil {
			fmt.Fprintln(w, "*NIL")
		} else if len(zi.meta) == 0 {
			fmt.Fprintln(w, "*(0)")
		} else {
			for k, fb := range zi.meta {
				fmt.Fprintln(w, "*", k)
				writeZidsLn(w, "]", fb.forward)
				writeZidsLn(w, "[", fb.backward)
			}
		}
	}

	zids = make(id.Slice, 0, len(ms.dead))
	for id := range ms.dead {
		zids = append(zids, id)
	}
	zids.Sort()
	for _, id := range zids {
		fmt.Fprintln(w, "~", id, ms.dead[id])
	}
}

func writeZidsLn(w io.Writer, prefix string, zids id.Slice) {
	io.WriteString(w, prefix)
	for _, zid := range zids {
		io.WriteString(w, " ")
		w.Write(zid.Bytes())
	}
	fmt.Fprintln(w)
}

Changes to index/memstore/refs.go.

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// under this license.
//-----------------------------------------------------------------------------

// Package memstore stored the index in main memory.
package memstore

import (
	"bytes"

	"zettelstore.de/z/domain/id"
)

func refsToString(refs []id.Zid) string {
	var buf bytes.Buffer
	for i, dref := range refs {
		if i > 0 {
			buf.WriteByte(' ')
		}
		buf.Write(dref.Bytes())
	}
	return buf.String()
}

func refsDiff(refsN, refsO []id.Zid) (newRefs, remRefs []id.Zid) {
	npos, opos := 0, 0
	for npos < len(refsN) && opos < len(refsO) {
		rn, ro := refsN[npos], refsO[opos]
		if rn == ro {
			npos++
			opos++
			continue







<
<



<
<
<
<
<
<
<
<
<
<
<
|







8
9
10
11
12
13
14


15
16
17











18
19
20
21
22
23
24
25
// under this license.
//-----------------------------------------------------------------------------

// Package memstore stored the index in main memory.
package memstore

import (


	"zettelstore.de/z/domain/id"
)












func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) {
	npos, opos := 0, 0
	for npos < len(refsN) && opos < len(refsO) {
		rn, ro := refsN[npos], refsO[opos]
		if rn == ro {
			npos++
			opos++
			continue
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
	}
	if opos < len(refsO) {
		remRefs = append(remRefs, refsO[opos:]...)
	}
	return newRefs, remRefs
}

func addRef(refs []id.Zid, ref id.Zid) []id.Zid {
	if len(refs) == 0 {
		return append(refs, ref)
	}
	for i, r := range refs {
		if r == ref {
			return refs
		}
		if r > ref {
			return append(refs[:i], append([]id.Zid{ref}, refs[i:]...)...)
		}
	}
	return append(refs, ref)
}

func remRefs(refs []id.Zid, rem []id.Zid) []id.Zid {
	if len(refs) == 0 || len(rem) == 0 {
		return refs
	}
	result := make([]id.Zid, 0, len(refs))
	rpos, dpos := 0, 0
	for rpos < len(refs) && dpos < len(rem) {
		rr, dr := refs[rpos], rem[dpos]
		if rr < dr {
			result = append(result, rr)
			rpos++
			continue







|








|





|



|







37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
	}
	if opos < len(refsO) {
		remRefs = append(remRefs, refsO[opos:]...)
	}
	return newRefs, remRefs
}

func addRef(refs id.Slice, ref id.Zid) id.Slice {
	if len(refs) == 0 {
		return append(refs, ref)
	}
	for i, r := range refs {
		if r == ref {
			return refs
		}
		if r > ref {
			return append(refs[:i], append(id.Slice{ref}, refs[i:]...)...)
		}
	}
	return append(refs, ref)
}

func remRefs(refs id.Slice, rem id.Slice) id.Slice {
	if len(refs) == 0 || len(rem) == 0 {
		return refs
	}
	result := make(id.Slice, 0, len(refs))
	rpos, dpos := 0, 0
	for rpos < len(refs) && dpos < len(rem) {
		rr, dr := refs[rpos], rem[dpos]
		if rr < dr {
			result = append(result, rr)
			rpos++
			continue
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
	}
	if rpos < len(refs) {
		result = append(result, refs[rpos:]...)
	}
	return result
}

func remRef(refs []id.Zid, ref id.Zid) []id.Zid {
	if refs != nil {
		for i, r := range refs {
			if r == ref {
				return append(refs[:i], refs[i+1:]...)
			}
			if r > ref {
				return refs
			}
		}
	}
	return refs
}







|
<
|
|
|
|
|
|
<




78
79
80
81
82
83
84
85

86
87
88
89
90
91

92
93
94
95
	}
	if rpos < len(refs) {
		result = append(result, refs[rpos:]...)
	}
	return result
}

func remRef(refs id.Slice, ref id.Zid) id.Slice {

	for i, r := range refs {
		if r == ref {
			return append(refs[:i], refs[i+1:]...)
		}
		if r > ref {
			return refs

		}
	}
	return refs
}

Changes to index/memstore/refs_test.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151

152
153
154
155
156
157
158
159
160
161
162

import (
	"testing"

	"zettelstore.de/z/domain/id"
)

func numsToRefs(nums []uint) []id.Zid {
	if nums == nil {
		return nil
	}
	refs := make([]id.Zid, 0, len(nums))
	for _, n := range nums {
		refs = append(refs, id.Zid(n))
	}
	return refs
}

func assertRefs(t *testing.T, i int, got []id.Zid, exp []uint) {
	t.Helper()
	if got == nil && exp != nil {
		t.Errorf("%d: got nil, but expected %v", i, exp)
		return
	}
	if got != nil && exp == nil {
		t.Errorf("%d: expected nil, but got %v", i, got)
		return
	}
	if len(got) != len(exp) {
		t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got))
		return
	}
	for p, n := range exp {
		if got := got[p]; got != id.Zid(n) {
			t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got)
		}
	}
}

func TestRefsToString(t *testing.T) {
	testcases := []struct {
		in  []uint
		exp string
	}{
		{nil, ""},
		{[]uint{}, ""},
		{[]uint{1}, "00000000000001"},
		{[]uint{1, 2}, "00000000000001 00000000000002"},
	}
	for i, tc := range testcases {
		got := refsToString(numsToRefs(tc.in))
		if got != tc.exp {
			t.Errorf("%d/%v: expected %q, but got %q", i, tc.in, tc.exp, got)
		}
	}
}

func TestRefsDiff(t *testing.T) {
	testcases := []struct {
		in1, in2   []uint
		exp1, exp2 []uint
	}{
		{nil, nil, nil, nil},
		{[]uint{1}, nil, []uint{1}, nil},
		{nil, []uint{1}, nil, []uint{1}},
		{[]uint{1}, []uint{1}, nil, nil},
		{[]uint{1, 2}, []uint{1}, []uint{2}, nil},
		{[]uint{1, 2}, []uint{1, 3}, []uint{2}, []uint{3}},
		{[]uint{1, 4}, []uint{1, 3}, []uint{4}, []uint{3}},
	}
	for i, tc := range testcases {
		got1, got2 := refsDiff(numsToRefs(tc.in1), numsToRefs(tc.in2))
		assertRefs(t, i, got1, tc.exp1)
		assertRefs(t, i, got2, tc.exp2)
	}
}

func TestAddRef(t *testing.T) {
	testcases := []struct {
		ref []uint
		zid uint
		exp []uint
	}{
		{nil, 5, []uint{5}},
		{[]uint{1}, 5, []uint{1, 5}},
		{[]uint{10}, 5, []uint{5, 10}},
		{[]uint{5}, 5, []uint{5}},
		{[]uint{1, 10}, 5, []uint{1, 5, 10}},
		{[]uint{1, 5, 10}, 5, []uint{1, 5, 10}},
	}
	for i, tc := range testcases {
		got := addRef(numsToRefs(tc.ref), id.Zid(tc.zid))
		assertRefs(t, i, got, tc.exp)
	}
}

func TestRemRefs(t *testing.T) {
	testcases := []struct {
		in1, in2 []uint
		exp      []uint
	}{
		{nil, nil, nil},
		{nil, []uint{}, nil},
		{[]uint{}, nil, []uint{}},
		{[]uint{}, []uint{}, []uint{}},
		{[]uint{1}, []uint{5}, []uint{1}},
		{[]uint{10}, []uint{5}, []uint{10}},
		{[]uint{1, 5}, []uint{5}, []uint{1}},
		{[]uint{5, 10}, []uint{5}, []uint{10}},
		{[]uint{1, 10}, []uint{5}, []uint{1, 10}},
		{[]uint{1}, []uint{2, 5}, []uint{1}},
		{[]uint{10}, []uint{2, 5}, []uint{10}},
		{[]uint{1, 5}, []uint{2, 5}, []uint{1}},
		{[]uint{5, 10}, []uint{2, 5}, []uint{10}},
		{[]uint{1, 2, 5}, []uint{2, 5}, []uint{1}},
		{[]uint{2, 5, 10}, []uint{2, 5}, []uint{10}},
		{[]uint{1, 10}, []uint{2, 5}, []uint{1, 10}},
		{[]uint{1}, []uint{5, 9}, []uint{1}},
		{[]uint{10}, []uint{5, 9}, []uint{10}},
		{[]uint{1, 5}, []uint{5, 9}, []uint{1}},
		{[]uint{5, 10}, []uint{5, 9}, []uint{10}},
		{[]uint{1, 5, 9}, []uint{5, 9}, []uint{1}},
		{[]uint{5, 9, 10}, []uint{5, 9}, []uint{10}},
		{[]uint{1, 10}, []uint{5, 9}, []uint{1, 10}},
	}
	for i, tc := range testcases {
		got := remRefs(numsToRefs(tc.in1), numsToRefs(tc.in2))
		assertRefs(t, i, got, tc.exp)
	}
}

func TestRemRef(t *testing.T) {
	testcases := []struct {
		ref []uint
		zid uint
		exp []uint
	}{
		{nil, 5, nil},
		{[]uint{}, 5, []uint{}},

		{[]uint{1}, 5, []uint{1}},
		{[]uint{10}, 5, []uint{10}},
		{[]uint{1, 5}, 5, []uint{1}},
		{[]uint{5, 10}, 5, []uint{10}},
		{[]uint{1, 5, 10}, 5, []uint{1, 10}},
	}
	for i, tc := range testcases {
		got := remRef(numsToRefs(tc.ref), id.Zid(tc.zid))
		assertRefs(t, i, got, tc.exp)
	}
}







<
<
<
<
<
<
<
<
<
<
<
|




















<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


|
|


|
|
|
|
|
|


|







|

|

|
|
|
|
|
|


|






|
|


|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|


|






|

|


|
>
|
|
|
|
|


|



13
14
15
16
17
18
19











20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40


















41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134

import (
	"testing"

	"zettelstore.de/z/domain/id"
)












func assertRefs(t *testing.T, i int, got, exp id.Slice) {
	t.Helper()
	if got == nil && exp != nil {
		t.Errorf("%d: got nil, but expected %v", i, exp)
		return
	}
	if got != nil && exp == nil {
		t.Errorf("%d: expected nil, but got %v", i, got)
		return
	}
	if len(got) != len(exp) {
		t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got))
		return
	}
	for p, n := range exp {
		if got := got[p]; got != id.Zid(n) {
			t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got)
		}
	}
}



















func TestRefsDiff(t *testing.T) {
	testcases := []struct {
		in1, in2   id.Slice
		exp1, exp2 id.Slice
	}{
		{nil, nil, nil, nil},
		{id.Slice{1}, nil, id.Slice{1}, nil},
		{nil, id.Slice{1}, nil, id.Slice{1}},
		{id.Slice{1}, id.Slice{1}, nil, nil},
		{id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil},
		{id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}},
		{id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}},
	}
	for i, tc := range testcases {
		got1, got2 := refsDiff(tc.in1, tc.in2)
		assertRefs(t, i, got1, tc.exp1)
		assertRefs(t, i, got2, tc.exp2)
	}
}

func TestAddRef(t *testing.T) {
	testcases := []struct {
		ref id.Slice
		zid uint
		exp id.Slice
	}{
		{nil, 5, id.Slice{5}},
		{id.Slice{1}, 5, id.Slice{1, 5}},
		{id.Slice{10}, 5, id.Slice{5, 10}},
		{id.Slice{5}, 5, id.Slice{5}},
		{id.Slice{1, 10}, 5, id.Slice{1, 5, 10}},
		{id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}},
	}
	for i, tc := range testcases {
		got := addRef(tc.ref, id.Zid(tc.zid))
		assertRefs(t, i, got, tc.exp)
	}
}

func TestRemRefs(t *testing.T) {
	testcases := []struct {
		in1, in2 id.Slice
		exp      id.Slice
	}{
		{nil, nil, nil},
		{nil, id.Slice{}, nil},
		{id.Slice{}, nil, id.Slice{}},
		{id.Slice{}, id.Slice{}, id.Slice{}},
		{id.Slice{1}, id.Slice{5}, id.Slice{1}},
		{id.Slice{10}, id.Slice{5}, id.Slice{10}},
		{id.Slice{1, 5}, id.Slice{5}, id.Slice{1}},
		{id.Slice{5, 10}, id.Slice{5}, id.Slice{10}},
		{id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}},
		{id.Slice{1}, id.Slice{2, 5}, id.Slice{1}},
		{id.Slice{10}, id.Slice{2, 5}, id.Slice{10}},
		{id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}},
		{id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}},
		{id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}},
		{id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}},
		{id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}},
		{id.Slice{1}, id.Slice{5, 9}, id.Slice{1}},
		{id.Slice{10}, id.Slice{5, 9}, id.Slice{10}},
		{id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}},
		{id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}},
		{id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}},
		{id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}},
		{id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}},
	}
	for i, tc := range testcases {
		got := remRefs(tc.in1, tc.in2)
		assertRefs(t, i, got, tc.exp)
	}
}

func TestRemRef(t *testing.T) {
	testcases := []struct {
		ref id.Slice
		zid uint
		exp id.Slice
	}{
		{nil, 5, nil},
		{id.Slice{}, 5, id.Slice{}},
		{id.Slice{5}, 5, id.Slice{}},
		{id.Slice{1}, 5, id.Slice{1}},
		{id.Slice{10}, 5, id.Slice{10}},
		{id.Slice{1, 5}, 5, id.Slice{1}},
		{id.Slice{5, 10}, 5, id.Slice{10}},
		{id.Slice{1, 5, 10}, 5, id.Slice{1, 10}},
	}
	for i, tc := range testcases {
		got := remRef(tc.ref, id.Zid(tc.zid))
		assertRefs(t, i, got, tc.exp)
	}
}

Changes to index/zettel.go.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

import (
	"zettelstore.de/z/domain/id"
)

// ZettelIndex contains all index data of a zettel.
type ZettelIndex struct {
	Zid      id.Zid                     // zid of the indexed zettel
	backrefs map[id.Zid]bool            // set of back references
	metarefs map[string]map[id.Zid]bool // references to inverse keys
	deadrefs map[id.Zid]bool            // set of dead references
}

// NewZettelIndex creates a new zettel index.
func NewZettelIndex(zid id.Zid) *ZettelIndex {
	return &ZettelIndex{
		Zid:      zid,
		backrefs: make(map[id.Zid]bool),
		metarefs: make(map[string]map[id.Zid]bool),
		deadrefs: make(map[id.Zid]bool),
	}
}

// AddBackRef adds a reference to a zettel where the current zettel links to
// without any more information.
func (zi *ZettelIndex) AddBackRef(zid id.Zid) {
	zi.backrefs[zid] = true
}

// AddMetaRef adds a named reference to a zettel. On that zettel, the given
// metadata key should point back to the current zettel.
func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) {
	if zids, ok := zi.metarefs[key]; ok {
		zids[zid] = true
		return
	}
	zi.metarefs[key] = map[id.Zid]bool{zid: true}
}

// AddDeadRef adds a dead reference to a zettel.
func (zi *ZettelIndex) AddDeadRef(zid id.Zid) {
	zi.deadrefs[zid] = true
}

// GetDeadRefs returns all dead references as a sorted list.
func (zi *ZettelIndex) GetDeadRefs() []id.Zid {
	return sortedZids(zi.deadrefs)
}

// GetBackRefs returns all back references as a sorted list.
func (zi *ZettelIndex) GetBackRefs() []id.Zid {
	return sortedZids(zi.backrefs)
}

// GetMetaRefs returns all meta references as a map of strings to a sorted list of references
func (zi *ZettelIndex) GetMetaRefs() map[string][]id.Zid {
	if len(zi.metarefs) == 0 {
		return nil
	}
	result := make(map[string][]id.Zid, len(zi.metarefs))
	for key, refs := range zi.metarefs {
		result[key] = sortedZids(refs)
	}
	return result
}

func sortedZids(refmap map[id.Zid]bool) []id.Zid {
	if l := len(refmap); l > 0 {
		result := make([]id.Zid, 0, l)
		for zid := range refmap {
			result = append(result, zid)
		}
		id.Sort(result)
		return result
	}
	return nil
}







|
|
|
|






|
|
|
















|








|
|



|
|



|



|

|



<
<
<
<
<
<
<
<
<
<
<
<
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77













import (
	"zettelstore.de/z/domain/id"
)

// ZettelIndex contains all index data of a zettel.
type ZettelIndex struct {
	Zid      id.Zid            // zid of the indexed zettel
	backrefs id.Set            // set of back references
	metarefs map[string]id.Set // references to inverse keys
	deadrefs id.Set            // set of dead references
}

// NewZettelIndex creates a new zettel index.
func NewZettelIndex(zid id.Zid) *ZettelIndex {
	return &ZettelIndex{
		Zid:      zid,
		backrefs: id.NewSet(),
		metarefs: make(map[string]id.Set),
		deadrefs: id.NewSet(),
	}
}

// AddBackRef adds a reference to a zettel where the current zettel links to
// without any more information.
func (zi *ZettelIndex) AddBackRef(zid id.Zid) {
	zi.backrefs[zid] = true
}

// AddMetaRef adds a named reference to a zettel. On that zettel, the given
// metadata key should point back to the current zettel.
func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) {
	if zids, ok := zi.metarefs[key]; ok {
		zids[zid] = true
		return
	}
	zi.metarefs[key] = id.NewSet(zid)
}

// AddDeadRef adds a dead reference to a zettel.
func (zi *ZettelIndex) AddDeadRef(zid id.Zid) {
	zi.deadrefs[zid] = true
}

// GetDeadRefs returns all dead references as a sorted list.
func (zi *ZettelIndex) GetDeadRefs() id.Slice {
	return zi.deadrefs.Sort()
}

// GetBackRefs returns all back references as a sorted list.
func (zi *ZettelIndex) GetBackRefs() id.Slice {
	return zi.backrefs.Sort()
}

// GetMetaRefs returns all meta references as a map of strings to a sorted list of references
func (zi *ZettelIndex) GetMetaRefs() map[string]id.Slice {
	if len(zi.metarefs) == 0 {
		return nil
	}
	result := make(map[string]id.Slice, len(zi.metarefs))
	for key, refs := range zi.metarefs {
		result[key] = refs.Sort()
	}
	return result
}












Changes to input/input.go.

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
			inp.Next()
		}
		inp.Ch = '\n'
		inp.Next()
	case '\n':
		inp.Next()
	}
	return
}

// SetPos allows to reset the read position.
func (inp *Input) SetPos(pos int) {
	inp.readPos = pos
	inp.Next()
}







<







85
86
87
88
89
90
91

92
93
94
95
96
97
98
			inp.Next()
		}
		inp.Ch = '\n'
		inp.Next()
	case '\n':
		inp.Next()
	}

}

// SetPos allows to reset the read position.
func (inp *Input) SetPos(pos int) {
	inp.readPos = pos
	inp.Next()
}

Changes to parser/cleanup.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
	if mn == nil {
		return
	}
	if !cv.doMark {
		cv.hasMark = true
		return
	}
	if len(mn.Text) == 0 {
		mn.Text = cv.addIdentifier("*", mn)
		return
	}
	mn.Text = cv.addIdentifier(mn.Text, mn)
}

// VisitFormat does nothing.







|







112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
	if mn == nil {
		return
	}
	if !cv.doMark {
		cv.hasMark = true
		return
	}
	if mn.Text == "" {
		mn.Text = cv.addIdentifier("*", mn)
		return
	}
	mn.Text = cv.addIdentifier(mn.Text, mn)
}

// VisitFormat does nothing.

Changes to parser/markdown/markdown.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
		result = append(result, &ast.BreakNode{Hard: false})
	}
	return result
}

// splitText transform the text into a sequence of TextNode and SpaceNode
func splitText(text string) ast.InlineSlice {
	if len(text) == 0 {
		return ast.InlineSlice{}
	}
	result := make(ast.InlineSlice, 0, 1)

	state := 0 // 0=unknown,1=non-spaces,2=spaces
	lastPos := 0
	for pos, ch := range text {







|







280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
		result = append(result, &ast.BreakNode{Hard: false})
	}
	return result
}

// splitText transform the text into a sequence of TextNode and SpaceNode
func splitText(text string) ast.InlineSlice {
	if text == "" {
		return ast.InlineSlice{}
	}
	result := make(ast.InlineSlice, 0, 1)

	state := 0 // 0=unknown,1=non-spaces,2=spaces
	lastPos := 0
	for pos, ch := range text {
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352










353
354
355
356
357
358
359
		panic(fmt.Sprintf("Unexpected state %v", state))
	}
	return result
}

// cleanText removes backslashes from TextNodes and expands entities
func cleanText(text string, cleanBS bool) string {
	if len(text) == 0 {
		return ""
	}
	lastPos := 0
	var sb strings.Builder
	for pos, ch := range text {
		if pos < lastPos {
			continue
		}
		switch ch {
		case '\\':
			if cleanBS && pos < len(text)-1 {
				switch b := text[pos+1]; b {
				case '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+',
					',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@',
					'[', '\\', ']', '^', '_', '`', '{', '|', '}', '~':
					sb.WriteString(text[lastPos:pos])
					sb.WriteByte(b)
					lastPos = pos + 2
				default:
				}
			}
		case '&':
			inp := input.NewInput(text[pos:])
			s, ok := inp.ScanEntity()
			if ok {
				sb.WriteString(text[lastPos:pos])
				sb.WriteString(s)
				lastPos = pos + inp.Pos
			}
		default:










		}
	}
	if lastPos == 0 {
		return text
	}
	if lastPos < len(text) {
		sb.WriteString(text[lastPos:])







|








<
<
<
<
<
<
<
<
<
<
<
<
<
|

|
<




|
>
>
>
>
>
>
>
>
>
>







315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330













331
332
333

334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
		panic(fmt.Sprintf("Unexpected state %v", state))
	}
	return result
}

// cleanText removes backslashes from TextNodes and expands entities
func cleanText(text string, cleanBS bool) string {
	if text == "" {
		return ""
	}
	lastPos := 0
	var sb strings.Builder
	for pos, ch := range text {
		if pos < lastPos {
			continue
		}













		if ch == '&' {
			inp := input.NewInput(text[pos:])
			if s, ok := inp.ScanEntity(); ok {

				sb.WriteString(text[lastPos:pos])
				sb.WriteString(s)
				lastPos = pos + inp.Pos
			}
			continue
		}
		if cleanBS && ch == '\\' && pos < len(text)-1 {
			switch b := text[pos+1]; b {
			case '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+',
				',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@',
				'[', '\\', ']', '^', '_', '`', '{', '|', '}', '~':
				sb.WriteString(text[lastPos:pos])
				sb.WriteByte(b)
				lastPos = pos + 2
			}
		}
	}
	if lastPos == 0 {
		return text
	}
	if lastPos < len(text) {
		sb.WriteString(text[lastPos:])
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
			Attrs: nil, //TODO
			Text:  cleanCodeSpan(string(node.Text(p.source))),
		},
	}
}

func cleanCodeSpan(text string) string {
	if len(text) == 0 {
		return ""
	}
	lastPos := 0
	var sb strings.Builder
	for pos, ch := range text {
		switch ch {
		case '\n':
			sb.WriteString(text[lastPos:pos])
			if pos < len(text)-1 {
				sb.WriteByte(' ')
			}
			lastPos = pos + 1
		}
	}







|





<
|







364
365
366
367
368
369
370
371
372
373
374
375
376

377
378
379
380
381
382
383
384
			Attrs: nil, //TODO
			Text:  cleanCodeSpan(string(node.Text(p.source))),
		},
	}
}

func cleanCodeSpan(text string) string {
	if text == "" {
		return ""
	}
	lastPos := 0
	var sb strings.Builder
	for pos, ch := range text {

		if ch == '\n' {
			sb.WriteString(text[lastPos:pos])
			if pos < len(text)-1 {
				sb.WriteByte(' ')
			}
			lastPos = pos + 1
		}
	}

Changes to parser/parser.go.

79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
	return ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk)
}

// ParseZettel parses the zettel based on the syntax.
func ParseZettel(zettel domain.Zettel, syntax string) *ast.ZettelNode {
	m := zettel.Meta
	inhMeta := runtime.AddDefaultValues(zettel.Meta)
	if len(syntax) == 0 {
		syntax, _ = inhMeta.Get(meta.KeySyntax)
	}
	title, _ := inhMeta.Get(meta.KeyTitle)
	parseMeta := inhMeta
	if syntax == meta.ValueSyntaxNone {
		parseMeta = m
	}







|







79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
	return ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk)
}

// ParseZettel parses the zettel based on the syntax.
func ParseZettel(zettel domain.Zettel, syntax string) *ast.ZettelNode {
	m := zettel.Meta
	inhMeta := runtime.AddDefaultValues(zettel.Meta)
	if syntax == "" {
		syntax, _ = inhMeta.Get(meta.KeySyntax)
	}
	title, _ := inhMeta.Get(meta.KeyTitle)
	parseMeta := inhMeta
	if syntax == meta.ValueSyntaxNone {
		parseMeta = m
	}

Changes to parser/plain/plain.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
	}
}

func readLines(inp *input.Input) (lines []string) {
	for {
		inp.EatEOL()
		posL := inp.Pos
		switch inp.Ch {
		case input.EOS:
			return lines
		}
		inp.SkipToEOL()
		lines = append(lines, inp.Src[posL:inp.Pos])
	}
}








|
<







55
56
57
58
59
60
61
62

63
64
65
66
67
68
69
	}
}

func readLines(inp *input.Input) (lines []string) {
	for {
		inp.EatEOL()
		posL := inp.Pos
		if inp.Ch == input.EOS {

			return lines
		}
		inp.SkipToEOL()
		lines = append(lines, inp.Src[posL:inp.Pos])
	}
}

Changes to parser/zettelmark/block.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
		if parentPos < 0 {
			return cp.lists[0], true
		}
		if prevItems := cp.lists[parentPos].Items; len(prevItems) > 0 {
			lastItem := len(prevItems) - 1
			prevItems[lastItem] = append(prevItems[lastItem], cp.lists[childPos])
		} else {
			cp.lists[parentPos].Items = []ast.ItemSlice{
				ast.ItemSlice{cp.lists[childPos]},
			}
		}
	}
	return nil, true
}

// parseDefTerm parses a term of a definition list.
func (cp *zmkP) parseDefTerm() (res ast.BlockNode, success bool) {







|
<
<







367
368
369
370
371
372
373
374


375
376
377
378
379
380
381
		if parentPos < 0 {
			return cp.lists[0], true
		}
		if prevItems := cp.lists[parentPos].Items; len(prevItems) > 0 {
			lastItem := len(prevItems) - 1
			prevItems[lastItem] = append(prevItems[lastItem], cp.lists[childPos])
		} else {
			cp.lists[parentPos].Items = []ast.ItemSlice{{cp.lists[childPos]}}


		}
	}
	return nil, true
}

// parseDefTerm parses a term of a definition list.
func (cp *zmkP) parseDefTerm() (res ast.BlockNode, success bool) {
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477




478




















479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494

495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
		inp.Next()
		if inp.Ch != ' ' {
			break
		}
		cnt++
	}
	if cp.lists != nil {
		// Identation for a list?
		if len(cp.lists) < cnt {
			cnt = len(cp.lists)
		}
		cp.lists = cp.lists[:cnt]
		if cnt == 0 {
			return nil, false
		}
		ln := cp.lists[cnt-1]
		pn := cp.parseLinePara()
		lbn := ln.Items[len(ln.Items)-1]
		if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok {
			lpn.Inlines = append(lpn.Inlines, pn.Inlines...)
		} else {
			ln.Items[len(ln.Items)-1] = append(ln.Items[len(ln.Items)-1], pn)
		}
		return nil, true
	}
	if cp.descrl != nil {




		// Indentation for definition list




















		defPos := len(cp.descrl.Descriptions) - 1
		if cnt < 1 || defPos < 0 {
			return nil, false
		}
		if len(cp.descrl.Descriptions[defPos].Descriptions) == 0 {
			// Continuation of a definition term
			for {
				in := cp.parseInline()
				if in == nil {
					return nil, true
				}
				cp.descrl.Descriptions[defPos].Term = append(cp.descrl.Descriptions[defPos].Term, in)
				if _, ok := in.(*ast.BreakNode); ok {
					return nil, true
				}
			}

		} else {
			// Continuation of a definition description
			pn := cp.parseLinePara()
			if pn == nil {
				return nil, false
			}
			descrPos := len(cp.descrl.Descriptions[defPos].Descriptions) - 1
			lbn := cp.descrl.Descriptions[defPos].Descriptions[descrPos]
			if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok {
				lpn.Inlines = append(lpn.Inlines, pn.Inlines...)
			} else {
				descrPos := len(cp.descrl.Descriptions[defPos].Descriptions) - 1
				cp.descrl.Descriptions[defPos].Descriptions[descrPos] = append(cp.descrl.Descriptions[defPos].Descriptions[descrPos], pn)
			}
			return nil, true
		}
	}
	return nil, false
}

// parseLinePara parses one line of inline material.
func (cp *zmkP) parseLinePara() *ast.ParaNode {
	pn := &ast.ParaNode{}
	for {
		in := cp.parseInline()







<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<


>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<
<
<







450
451
452
453
454
455
456






457










458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516



517
518
519
520
521
522
523
		inp.Next()
		if inp.Ch != ' ' {
			break
		}
		cnt++
	}
	if cp.lists != nil {






		return nil, cp.parseIndentForList(cnt)










	}
	if cp.descrl != nil {
		return nil, cp.parseIndentForDescription(cnt)
	}
	return nil, false
}

func (cp *zmkP) parseIndentForList(cnt int) bool {
	if len(cp.lists) < cnt {
		cnt = len(cp.lists)
	}
	cp.lists = cp.lists[:cnt]
	if cnt == 0 {
		return false
	}
	ln := cp.lists[cnt-1]
	pn := cp.parseLinePara()
	lbn := ln.Items[len(ln.Items)-1]
	if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok {
		lpn.Inlines = append(lpn.Inlines, pn.Inlines...)
	} else {
		ln.Items[len(ln.Items)-1] = append(ln.Items[len(ln.Items)-1], pn)
	}
	return true
}

func (cp *zmkP) parseIndentForDescription(cnt int) bool {
	defPos := len(cp.descrl.Descriptions) - 1
	if cnt < 1 || defPos < 0 {
		return false
	}
	if len(cp.descrl.Descriptions[defPos].Descriptions) == 0 {
		// Continuation of a definition term
		for {
			in := cp.parseInline()
			if in == nil {
				return true
			}
			cp.descrl.Descriptions[defPos].Term = append(cp.descrl.Descriptions[defPos].Term, in)
			if _, ok := in.(*ast.BreakNode); ok {
				return true
			}
		}
	}

	// Continuation of a definition description
	pn := cp.parseLinePara()
	if pn == nil {
		return false
	}
	descrPos := len(cp.descrl.Descriptions[defPos].Descriptions) - 1
	lbn := cp.descrl.Descriptions[defPos].Descriptions[descrPos]
	if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok {
		lpn.Inlines = append(lpn.Inlines, pn.Inlines...)
	} else {
		descrPos := len(cp.descrl.Descriptions[defPos].Descriptions) - 1
		cp.descrl.Descriptions[defPos].Descriptions[descrPos] = append(cp.descrl.Descriptions[defPos].Descriptions[descrPos], pn)
	}
	return true



}

// parseLinePara parses one line of inline material.
func (cp *zmkP) parseLinePara() *ast.ParaNode {
	pn := &ast.ParaNode{}
	for {
		in := cp.parseInline()

Changes to parser/zettelmark/inline.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
			case '^':
				in, success = cp.parseFootnote()
			case '!':
				in, success = cp.parseMark()
			}
		case '{':
			inp.Next()
			switch inp.Ch {
			case '{':
				in, success = cp.parseImage()
			}
		case '#':
			return cp.parseTag()
		case '%':
			in, success = cp.parseComment()
		case '/', '*', '_', '~', '\'', '^', ',', '<', '"', ';', ':':







|
<







59
60
61
62
63
64
65
66

67
68
69
70
71
72
73
			case '^':
				in, success = cp.parseFootnote()
			case '!':
				in, success = cp.parseMark()
			}
		case '{':
			inp.Next()
			if inp.Ch == '{' {

				in, success = cp.parseImage()
			}
		case '#':
			return cp.parseTag()
		case '%':
			in, success = cp.parseComment()
		case '/', '*', '_', '~', '\'', '^', ',', '<', '"', ';', ':':

Changes to parser/zettelmark/node.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"zettelstore.de/z/ast"
)

// Internal nodes for parsing zettelmark. These will be removed in
// post-processing.

// nullItemNode specifies a removable placeholder for an item block.
type nullItemNode struct {
	ast.ItemNode
}

func (nn *nullItemNode) blockNode() {}
func (nn *nullItemNode) itemNode()  {}

// Accept a visitor and visit the node.
func (nn *nullItemNode) Accept(v ast.Visitor) {}

// nullDescriptionNode specifies a removable placeholder.
type nullDescriptionNode struct {
	ast.DescriptionNode
}

func (nn *nullDescriptionNode) blockNode()       {}
func (nn *nullDescriptionNode) descriptionNode() {}

// Accept a visitor and visit the node.
func (nn *nullDescriptionNode) Accept(v ast.Visitor) {}

|


















|




<
<
<








<
<
<


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25



26
27
28
29
30
31
32
33



34
35
//-----------------------------------------------------------------------------
// Copyright (c) 2020-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 zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"zettelstore.de/z/ast"
)

// Internal nodes for parsing zettelmark. These will be removed in
// post-processing.

// nullItemNode specifies a removable placeholder for an item node.
type nullItemNode struct {
	ast.ItemNode
}




// Accept a visitor and visit the node.
func (nn *nullItemNode) Accept(v ast.Visitor) {}

// nullDescriptionNode specifies a removable placeholder.
type nullDescriptionNode struct {
	ast.DescriptionNode
}




// Accept a visitor and visit the node.
func (nn *nullDescriptionNode) Accept(v ast.Visitor) {}

Changes to parser/zettelmark/post-processor.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// VisitPara post-processes a paragraph.
func (pp *postProcessor) VisitPara(pn *ast.ParaNode) {
	if pn != nil {
		pn.Inlines = pp.processInlineSlice(pn.Inlines)
	}
}

// VisitVerbatim post-processes a verbatim block.
func (pp *postProcessor) VisitVerbatim(vn *ast.VerbatimNode) {}

// VisitRegion post-processes a region.
func (pp *postProcessor) VisitRegion(rn *ast.RegionNode) {
	oldVerse := pp.inVerse
	if rn.Code == ast.RegionVerse {
		pp.inVerse = true
	}
	rn.Blocks = pp.processBlockSlice(rn.Blocks)
	pp.inVerse = oldVerse
	rn.Inlines = pp.processInlineSlice(rn.Inlines)
}

// VisitHeading post-processes a heading.
func (pp *postProcessor) VisitHeading(hn *ast.HeadingNode) {
	hn.Inlines = pp.processInlineSlice(hn.Inlines)
}

// VisitHRule post-processes a horizontal rule.
func (pp *postProcessor) VisitHRule(hn *ast.HRuleNode) {}

// VisitList post-processes a list.
func (pp *postProcessor) VisitNestedList(ln *ast.NestedListNode) {
	for i, item := range ln.Items {
		ln.Items[i] = pp.processItemSlice(item)
	}







|


















|







37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// VisitPara post-processes a paragraph.
func (pp *postProcessor) VisitPara(pn *ast.ParaNode) {
	if pn != nil {
		pn.Inlines = pp.processInlineSlice(pn.Inlines)
	}
}

// VisitVerbatim does nothing, no post-processing needed.
func (pp *postProcessor) VisitVerbatim(vn *ast.VerbatimNode) {}

// VisitRegion post-processes a region.
func (pp *postProcessor) VisitRegion(rn *ast.RegionNode) {
	oldVerse := pp.inVerse
	if rn.Code == ast.RegionVerse {
		pp.inVerse = true
	}
	rn.Blocks = pp.processBlockSlice(rn.Blocks)
	pp.inVerse = oldVerse
	rn.Inlines = pp.processInlineSlice(rn.Inlines)
}

// VisitHeading post-processes a heading.
func (pp *postProcessor) VisitHeading(hn *ast.HeadingNode) {
	hn.Inlines = pp.processInlineSlice(hn.Inlines)
}

// VisitHRule does nothing, no post-processing needed.
func (pp *postProcessor) VisitHRule(hn *ast.HRuleNode) {}

// VisitList post-processes a list.
func (pp *postProcessor) VisitNestedList(ln *ast.NestedListNode) {
	for i, item := range ln.Items {
		ln.Items[i] = pp.processItemSlice(item)
	}
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
	}
	if len(tn.Rows) > 0 && isHeaderRow(tn.Rows[0]) {
		tn.Header = tn.Rows[0]
		tn.Rows = tn.Rows[1:]
		for pos, cell := range tn.Header {
			if inlines := cell.Inlines; len(inlines) > 0 {
				if textNode, ok := inlines[0].(*ast.TextNode); ok {
					if strings.HasPrefix(textNode.Text, "=") {
						textNode.Text = textNode.Text[1:]
					}
				}
				if textNode, ok := inlines[len(inlines)-1].(*ast.TextNode); ok {
					if tnl := len(textNode.Text); tnl > 0 {
						if align := getAlignment(textNode.Text[tnl-1]); align != ast.AlignDefault {
							tn.Align[pos] = align
							textNode.Text = textNode.Text[0 : tnl-1]
						}







|
<
<







89
90
91
92
93
94
95
96


97
98
99
100
101
102
103
	}
	if len(tn.Rows) > 0 && isHeaderRow(tn.Rows[0]) {
		tn.Header = tn.Rows[0]
		tn.Rows = tn.Rows[1:]
		for pos, cell := range tn.Header {
			if inlines := cell.Inlines; len(inlines) > 0 {
				if textNode, ok := inlines[0].(*ast.TextNode); ok {
					textNode.Text = strings.TrimPrefix(textNode.Text, "=")


				}
				if textNode, ok := inlines[len(inlines)-1].(*ast.TextNode); ok {
					if tnl := len(textNode.Text); tnl > 0 {
						if align := getAlignment(textNode.Text[tnl-1]); align != ast.AlignDefault {
							tn.Align[pos] = align
							textNode.Text = textNode.Text[0 : tnl-1]
						}
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
		ins[toPos] = nil // Kill node to enable garbage collection
	}
	return toPos
}

func (pp *postProcessor) processInlineSliceInplace(ins ast.InlineSlice) {
	for _, in := range ins {
		switch n := in.(type) {
		case *ast.TextNode:
			if n.Text == "..." {
				n.Text = "\u2026"
			} else if len(n.Text) == 4 && strings.IndexByte(",;:!?", n.Text[3]) >= 0 && n.Text[:3] == "..." {
				n.Text = "\u2026" + n.Text[3:]
			}
		}
	}
}







<
|








435
436
437
438
439
440
441

442
443
444
445
446
447
448
449
450
		ins[toPos] = nil // Kill node to enable garbage collection
	}
	return toPos
}

func (pp *postProcessor) processInlineSliceInplace(ins ast.InlineSlice) {
	for _, in := range ins {

		if n, ok := in.(*ast.TextNode); ok {
			if n.Text == "..." {
				n.Text = "\u2026"
			} else if len(n.Text) == 4 && strings.IndexByte(",;:!?", n.Text[3]) >= 0 && n.Text[:3] == "..." {
				n.Text = "\u2026" + n.Text[3:]
			}
		}
	}
}

Changes to parser/zettelmark/zettelmark.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
			updateAttrs(attrs, key, inp.Src[posV:inp.Pos])
			return true
		}
		inp.Next()
	}
}

func updateAttrs(attrs map[string]string, key string, val string) {
	if prevVal := attrs[key]; len(prevVal) > 0 {
		attrs[key] = prevVal + " " + val
	} else {
		attrs[key] = val
	}
}








|







135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
			updateAttrs(attrs, key, inp.Src[posV:inp.Pos])
			return true
		}
		inp.Next()
	}
}

func updateAttrs(attrs map[string]string, key, val string) {
	if prevVal := attrs[key]; len(prevVal) > 0 {
		attrs[key] = prevVal + " " + val
	} else {
		attrs[key] = val
	}
}

Changes to parser/zettelmark/zettelmark_test.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892

	for _, k := range keys {
		tv.b.WriteByte(' ')
		tv.b.WriteString(k)
		v := a.Attrs[k]
		if len(v) > 0 {
			tv.b.WriteByte('=')
			if strings.IndexRune(v, ' ') >= 0 {
				tv.b.WriteByte('"')
				tv.b.WriteString(v)
				tv.b.WriteByte('"')
			} else {
				tv.b.WriteString(v)
			}
		}
	}

	tv.b.WriteByte(']')
}







|











874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892

	for _, k := range keys {
		tv.b.WriteByte(' ')
		tv.b.WriteString(k)
		v := a.Attrs[k]
		if len(v) > 0 {
			tv.b.WriteByte('=')
			if strings.ContainsRune(v, ' ') {
				tv.b.WriteByte('"')
				tv.b.WriteString(v)
				tv.b.WriteByte('"')
			} else {
				tv.b.WriteString(v)
			}
		}
	}

	tv.b.WriteByte(']')
}

Changes to place/constplace/constdata.go.

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57


















58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147

148

149
150
151
152
153
154
155
156
157
158
159
160
161
162

163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213

214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// under this license.
//-----------------------------------------------------------------------------

// Package constplace stores zettel inside the executable.
package constplace

import (
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

const (
	syntaxTemplate = "mustache"
)

var constZettelMap = map[id.Zid]constZettel{
	id.ConfigurationZid: constZettel{
		constHeader{
			meta.KeyTitle:      "Zettelstore Runtime Configuration",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityOwner,
			meta.KeySyntax:     meta.ValueSyntaxNone,
		},
		"",
	},

	id.BaseTemplateZid: constZettel{
		constHeader{
			meta.KeyTitle:      "Zettelstore Base HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		domain.NewContent(
			`<!DOCTYPE html>
<html{{#Lang}} lang="{{Lang}}"{{/Lang}}>
<head>
<meta charset="utf-8">
<meta name="referrer" content="same-origin">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Zettelstore">
{{{MetaHeader}}}
<link rel="stylesheet" href="{{{StylesheetURL}}}">
{{{Header}}}
<title>{{Title}}</title>
</head>
<body>
<nav class="zs-menu">
<a href="{{{HomeURL}}}">Home</a>


















<div class="zs-dropdown">
<button>Lists</button>
<nav class="zs-dropdown-content">
<a href="{{{ListZettelURL}}}">List Zettel</a>
<a href="{{{ListRolesURL}}}">List Roles</a>
<a href="{{{ListTagsURL}}}">List Tags</a>
</nav>
</div>
{{#CanCreate}}
<div class="zs-dropdown">
<button>New</button>
<nav class="zs-dropdown-content">
{{#NewZettelLinks}}
<a href="{{{URL}}}">{{Text}}</a>
{{/NewZettelLinks}}
</nav>
</div>
{{/CanCreate}}
{{#WithAuth}}
<div class="zs-dropdown">
<button>User</button>
<nav class="zs-dropdown-content">
{{#UserIsValid}}
<a href="{{{UserZettelURL}}}">{{UserIdent}}</a>
<a href="{{{UserLogoutURL}}}">Logout</a>
{{/UserIsValid}}
{{^UserIsValid}}
<a href="{{{LoginURL}}}">Login</a>
{{/UserIsValid}}
{{#CanReload}}
<a href="{{{ReloadURL}}}">Reload</a>
{{/CanReload}}
</nav>
</div>
{{/WithAuth}}
{{{Menu}}}
<form action="{{{SearchURL}}}">
<input type="text" placeholder="Search.." name="s">
</form>
</nav>
<main class="content">
{{{Content}}}
</main>
{{#FooterHTML}}
<footer>
{{{FooterHTML}}}
</footer>
{{/FooterHTML}}
</body>
</html>`,
		),
	},

	id.LoginTemplateZid: constZettel{
		constHeader{
			meta.KeyTitle:      "Zettelstore Login Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		domain.NewContent(
			`<article>
<header>
<h1>{{Title}}</h1>
</header>
{{#Retry}}
<div class="zs-indication zs-error">Wrong user name / password. Try again.</div>
{{/Retry}}
<form method="POST" action="?_format=html">
<div>
<label for="username">User name</label>
<input class="zs-input" type="text" id="username" name="username" placeholder="Your user name.." autofocus>
</div>
<div>
<label for="password">Password</label>
<input class="zs-input" type="password" id="password" name="password" placeholder="Your password..">
</div>
<input class="zs-button" type="submit" value="Login">
</form>
</article>`,
		)},

	id.ListTemplateZid: constZettel{
		constHeader{
			meta.KeyTitle:      "Zettelstore List Meta HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		domain.NewContent(

			`<h1>{{Title}}</h1>

<ul>
{{#Metas}}<li><a href="{{{URL}}}">{{{Title}}}</a></li>
{{/Metas}}</ul>
{{#HasPrevNext}}
<p>
{{#HasPrev}}
<a href="{{{PrevURL}}}" rel="prev">Prev</a>
{{#HasNext}},{{/HasNext}}
{{/HasPrev}}
{{#HasNext}}
<a href="{{{NextURL}}}" rel="next">Next</a>
{{/HasNext}}
</p>
{{/HasPrevNext}}`)},


	id.DetailTemplateZid: constZettel{
		constHeader{
			meta.KeyTitle:      "Zettelstore Detail HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		domain.NewContent(
			`<article>
<header>
<h1>{{{HTMLTitle}}}</h1>
<div class="zs-meta">
{{#CanWrite}}<a href="{{{EditURL}}}">Edit</a> &#183;{{/CanWrite}}
{{Zid}} &#183;
<a href="{{{InfoURL}}}">Info</a> &#183;
(<a href="{{{RoleURL}}}">{{RoleText}}</a>)
{{#HasTags}}&#183; {{#Tags}} <a href="{{{URL}}}">{{Text}}</a>{{/Tags}}{{/HasTags}}
{{#CanCopy}}&#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}
{{#CanFolge}}&#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}
{{#CanNew}}&#183; <a href="{{{NewURL}}}">New</a>{{/CanNew}}
{{#FolgeRefs}}<br>Folge: {{{FolgeRefs}}}{{/FolgeRefs}}
{{#PrecursorRefs}}<br>Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}}
{{#HasExtURL}}<br>URL: <a href="{{{ExtURL}}}"{{{ExtNewWindow}}}>{{ExtURL}}</a>{{/HasExtURL}}
</div>
</header>
{{{Content}}}
{{#HasBackLinks}}
<details>
<summary>Links to this zettel</summary>
<ul>
{{#BackLinks}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/BackLinks}}
</ul>
</details>
{{/HasBackLinks}}
</article>`)},

	id.InfoTemplateZid: constZettel{
		constHeader{
			meta.KeyTitle:      "Zettelstore Info HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		domain.NewContent(
			`<article>
<header>
<h1>Information for Zettel {{Zid}}</h1>
<a href="{{{WebURL}}}">Web</a>

{{#CanWrite}} &#183; <a href="{{{EditURL}}}">Edit</a>{{/CanWrite}}
{{#CanFolge}} &#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}
{{#CanCopy}} &#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}
{{#CanNew}} &#183; <a href="{{{NewURL}}}">New</a>{{/CanNew}}
{{#CanRename}}&#183; <a href="{{{RenameURL}}}">Rename</a>{{/CanRename}}
{{#CanDelete}}&#183; <a href="{{{DeleteURL}}}">Delete</a>{{/CanDelete}}
</header>
<h2>Interpreted Meta Data</h2>
<table>{{#MetaData}}<tr><td>{{Key}}</td><td>{{{Value}}}</td></tr>{{/MetaData}}</table>
{{#HasLinks}}
<h2>References</h2>
{{#HasLocLinks}}
<h3>Local</h3>
<ul>
{{#LocLinks}}







<









|









|






<
|








<





>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


















<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<













|
<
<

|






<
|

















|
<

|






|
>
|
>

|











|
>

|






<
|










<
















|

|






<
|



>



<



|







8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

41
42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90


















91
92
93
94
95
96
97
98
99
100
101
102
103
104


105
106
107
108
109
110
111
112

113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131

132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166

167
168
169
170
171
172
173
174
175
176
177

178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202

203
204
205
206
207
208
209
210

211
212
213
214
215
216
217
218
219
220
221
// under this license.
//-----------------------------------------------------------------------------

// Package constplace stores zettel inside the executable.
package constplace

import (

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

const (
	syntaxTemplate = "mustache"
)

var constZettelMap = map[id.Zid]constZettel{
	id.ConfigurationZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Runtime Configuration",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityOwner,
			meta.KeySyntax:     meta.ValueSyntaxNone,
		},
		"",
	},

	id.BaseTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},

		`<!DOCTYPE html>
<html{{#Lang}} lang="{{Lang}}"{{/Lang}}>
<head>
<meta charset="utf-8">
<meta name="referrer" content="same-origin">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Zettelstore">
{{{MetaHeader}}}
<link rel="stylesheet" href="{{{StylesheetURL}}}">

<title>{{Title}}</title>
</head>
<body>
<nav class="zs-menu">
<a href="{{{HomeURL}}}">Home</a>
{{#WithUser}}
<div class="zs-dropdown">
<button>User</button>
<nav class="zs-dropdown-content">
{{#WithAuth}}
{{#UserIsValid}}
<a href="{{{UserZettelURL}}}">{{UserIdent}}</a>
{{/UserIsValid}}
{{^UserIsValid}}
<a href="{{{LoginURL}}}">Login</a>
{{/UserIsValid}}
{{#UserIsValid}}
<a href="{{{UserLogoutURL}}}">Logout</a>
{{/UserIsValid}}
{{/WithAuth}}
</nav>
</div>
{{/WithUser}}
<div class="zs-dropdown">
<button>Lists</button>
<nav class="zs-dropdown-content">
<a href="{{{ListZettelURL}}}">List Zettel</a>
<a href="{{{ListRolesURL}}}">List Roles</a>
<a href="{{{ListTagsURL}}}">List Tags</a>
</nav>
</div>
{{#CanCreate}}
<div class="zs-dropdown">
<button>New</button>
<nav class="zs-dropdown-content">
{{#NewZettelLinks}}
<a href="{{{URL}}}">{{Text}}</a>
{{/NewZettelLinks}}
</nav>
</div>
{{/CanCreate}}


















<form action="{{{SearchURL}}}">
<input type="text" placeholder="Search.." name="s">
</form>
</nav>
<main class="content">
{{{Content}}}
</main>
{{#FooterHTML}}
<footer>
{{{FooterHTML}}}
</footer>
{{/FooterHTML}}
</body>
</html>`},



	id.LoginTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Login Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},

		`<article>
<header>
<h1>{{Title}}</h1>
</header>
{{#Retry}}
<div class="zs-indication zs-error">Wrong user name / password. Try again.</div>
{{/Retry}}
<form method="POST" action="?_format=html">
<div>
<label for="username">User name</label>
<input class="zs-input" type="text" id="username" name="username" placeholder="Your user name.." autofocus>
</div>
<div>
<label for="password">Password</label>
<input class="zs-input" type="password" id="password" name="password" placeholder="Your password..">
</div>
<input class="zs-button" type="submit" value="Login">
</form>
</article>`},


	id.ListTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Meta HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		`<nav>
<header>
<h1>{{Title}}</h1>
</header>
<ul>
{{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li>
{{/Metas}}</ul>
{{#HasPrevNext}}
<p>
{{#HasPrev}}
<a href="{{{PrevURL}}}" rel="prev">Prev</a>
{{#HasNext}},{{/HasNext}}
{{/HasPrev}}
{{#HasNext}}
<a href="{{{NextURL}}}" rel="next">Next</a>
{{/HasNext}}
</p>
{{/HasPrevNext}}
</nav>`},

	id.DetailTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Detail HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},

		`<article>
<header>
<h1>{{{HTMLTitle}}}</h1>
<div class="zs-meta">
{{#CanWrite}}<a href="{{{EditURL}}}">Edit</a> &#183;{{/CanWrite}}
{{Zid}} &#183;
<a href="{{{InfoURL}}}">Info</a> &#183;
(<a href="{{{RoleURL}}}">{{RoleText}}</a>)
{{#HasTags}}&#183; {{#Tags}} <a href="{{{URL}}}">{{Text}}</a>{{/Tags}}{{/HasTags}}
{{#CanCopy}}&#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}
{{#CanFolge}}&#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}

{{#FolgeRefs}}<br>Folge: {{{FolgeRefs}}}{{/FolgeRefs}}
{{#PrecursorRefs}}<br>Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}}
{{#HasExtURL}}<br>URL: <a href="{{{ExtURL}}}"{{{ExtNewWindow}}}>{{ExtURL}}</a>{{/HasExtURL}}
</div>
</header>
{{{Content}}}
{{#HasBackLinks}}
<details>
<summary>Links to this zettel</summary>
<ul>
{{#BackLinks}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/BackLinks}}
</ul>
</details>
{{/HasBackLinks}}
</article>`},

	id.InfoTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Info HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},

		`<article>
<header>
<h1>Information for Zettel {{Zid}}</h1>
<a href="{{{WebURL}}}">Web</a>
&#183; <a href="{{{ContextURL}}}">Context</a>
{{#CanWrite}} &#183; <a href="{{{EditURL}}}">Edit</a>{{/CanWrite}}
{{#CanFolge}} &#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}
{{#CanCopy}} &#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}

{{#CanRename}}&#183; <a href="{{{RenameURL}}}">Rename</a>{{/CanRename}}
{{#CanDelete}}&#183; <a href="{{{DeleteURL}}}">Delete</a>{{/CanDelete}}
</header>
<h2>Interpreted Metadata</h2>
<table>{{#MetaData}}<tr><td>{{Key}}</td><td>{{{Value}}}</td></tr>{{/MetaData}}</table>
{{#HasLinks}}
<h2>References</h2>
{{#HasLocLinks}}
<h3>Local</h3>
<ul>
{{#LocLinks}}
244
245
246
247
248
249
250
251







252
















253
254
255
256
257
258
259
260
261
{{#Matrix}}
<tr>
{{#Elements}}{{#HasURL}}<td><a href="{{{URL}}}">{{Text}}</td>{{/HasURL}}{{^HasURL}}<th>{{Text}}</th>{{/HasURL}}
{{/Elements}}
</tr>
{{/Matrix}}
</table>
</article>`),







	},

















	id.FormTemplateZid: constZettel{
		constHeader{
			meta.KeyTitle:      "Zettelstore Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		`<article>







|
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|







237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
{{#Matrix}}
<tr>
{{#Elements}}{{#HasURL}}<td><a href="{{{URL}}}">{{Text}}</td>{{/HasURL}}{{^HasURL}}<th>{{Text}}</th>{{/HasURL}}
{{/Elements}}
</tr>
{{/Matrix}}
</table>
</article>`},

	id.ContextTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Context HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		`<nav>
<header>
<h1>{{Title}}</h1>
<div class="zs-meta">
<a href="{{{InfoURL}}}">Info</a>
&#183; <a href="?dir=backward">Backward</a>
&#183; <a href="?dir=both">Both</a>
&#183; <a href="?dir=forward">Forward</a>
&#183; Depth:{{#Depths}}&#x2000;<a href="{{{URL}}}">{{{Text}}}</a>{{/Depths}}
</div>
</header>
<p><a href="{{{Start.URL}}}">{{{Start.Text}}}</a></p>
<ul>
{{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li>
{{/Metas}}</ul>
</nav>`},

	id.FormTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		`<article>
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
<div>
<label for="syntax">Syntax</label>
<input class="zs-input" type="text" id="syntax" name="syntax" placeholder="syntax.." value="{{MetaSyntax}}">
</div>
<div>
{{#IsTextContent}}
<label for="content">Content</label>
<textarea class="zs-input zs-content" id="meta" name="content" rows="20" placeholder="Your content..">
{{Content}}
</textarea>
{{/IsTextContent}}
</div>
<input class="zs-button" type="submit" value="Submit">
</form>
</article>`,
	},

	id.RenameTemplateZid: constZettel{
		constHeader{
			meta.KeyTitle:      "Zettelstore Rename Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		`<article>







|
<
<







|







302
303
304
305
306
307
308
309


310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
<div>
<label for="syntax">Syntax</label>
<input class="zs-input" type="text" id="syntax" name="syntax" placeholder="syntax.." value="{{MetaSyntax}}">
</div>
<div>
{{#IsTextContent}}
<label for="content">Content</label>
<textarea class="zs-input zs-content" id="meta" name="content" rows="20" placeholder="Your content..">{{Content}}</textarea>


{{/IsTextContent}}
</div>
<input class="zs-button" type="submit" value="Submit">
</form>
</article>`,
	},

	id.RenameTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Rename Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		`<article>
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
{{#MetaPairs}}
<dt>{{Key}}:</dt><dd>{{Value}}</dd>
{{/MetaPairs}}
</dl>
</article>`,
	},

	id.DeleteTemplateZid: constZettel{
		constHeader{
			meta.KeyTitle:      "Zettelstore Delete HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		`<article>







|







338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
{{#MetaPairs}}
<dt>{{Key}}:</dt><dd>{{Value}}</dd>
{{/MetaPairs}}
</dl>
</article>`,
	},

	id.DeleteTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Delete HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		`<article>
348
349
350
351
352
353
354
355
356
357
358
359
360
361


362

363
364
365
366
367
368
369
370
371
372
373
374


375
376
377
378

379
380
381
382
383
384
385
386
387
388
389
390
<form method="POST">
<input class="zs-button" type="submit" value="Delete">
</form>
</article>
{{end}}`,
	},

	id.RolesTemplateZid: constZettel{
		constHeader{
			meta.KeyTitle:      "Zettelstore List Roles HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},


		`<h1>Currently used roles</h1>

<ul>
{{#Roles}}<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/Roles}}</ul>`,
	},

	id.TagsTemplateZid: constZettel{
		constHeader{
			meta.KeyTitle:      "Zettelstore List Tags HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},


		`<h1>Currently used tags</h1>
<div class="zs-meta">
<a href="{{{#ListTagsURL}}}">All</a>{{#MinCounts}}, <a href="{{{URL}}}">{{Count}}</a>{{/MinCounts}}
</div>

{{#Tags}} <a href="{{{URL}}}" style="font-size:{{Size}}%">{{Name}}</a><sup>{{Count}}</sup>
{{/Tags}}`,
	},

	id.BaseCSSZid: constZettel{
		constHeader{
			meta.KeyTitle:      "Zettelstore Base CSS",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			meta.KeySyntax:     "css",
		},
		`/* Default CSS */







|






>
>
|
>


|
|

|






>
>
|



>

|
|

|







362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
<form method="POST">
<input class="zs-button" type="submit" value="Delete">
</form>
</article>
{{end}}`,
	},

	id.RolesTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Roles HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		`<nav>
<header>
<h1>Currently used roles</h1>
</header>
<ul>
{{#Roles}}<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/Roles}}</ul>
</nav>`},

	id.TagsTemplateZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Tags HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
			meta.KeySyntax:     syntaxTemplate,
		},
		`<nav>
<header>
<h1>Currently used tags</h1>
<div class="zs-meta">
<a href="{{{#ListTagsURL}}}">All</a>{{#MinCounts}}, <a href="{{{URL}}}">{{Count}}</a>{{/MinCounts}}
</div>
</header>
{{#Tags}} <a href="{{{URL}}}" style="font-size:{{Size}}%">{{Name}}</a><sup>{{Count}}</sup>
{{/Tags}}
</nav>`},

	id.BaseCSSZid: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base CSS",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeyVisibility: meta.ValueVisibilityPublic,
			meta.KeySyntax:     "css",
		},
		`/* Default CSS */
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
  border: 1px solid hsl(210, 5%, 70%);
  border-radius: .25rem;
  padding: .1rem .2rem;
  font-size: 75%;
}
.zs-meta {
  font-size:.75rem;
  color:#888;
  margin-bottom:1rem;
}
.zs-meta a {
  color:#888;
}
h1+.zs-meta {
  margin-top:-1rem;
}
details > summary {
  width: 100%;
  background-color: #eee;







|



|







653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
  border: 1px solid hsl(210, 5%, 70%);
  border-radius: .25rem;
  padding: .1rem .2rem;
  font-size: 75%;
}
.zs-meta {
  font-size:.75rem;
  color:#444;
  margin-bottom:1rem;
}
.zs-meta a {
  color:#444;
}
h1+.zs-meta {
  margin-top:-1rem;
}
details > summary {
  width: 100%;
  background-color: #eee;
662
663
664
665
666
667
668

669
670





671




672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693






694

695













































@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

}
`,





	},





	id.TemplateNewZettelZid: constZettel{
		constHeader{
			meta.KeyTitle:   "New Zettel",
			meta.KeyRole:    meta.ValueRoleNewTemplate,
			meta.KeyNewRole: meta.ValueRoleZettel,
			meta.KeySyntax:  meta.ValueSyntaxZmk,
		},
		"",
	},

	id.TemplateNewUserZid: constZettel{
		constHeader{
			meta.KeyTitle:      "New User",
			meta.KeyRole:       meta.ValueRoleNewTemplate,
			meta.KeyNewRole:    meta.ValueRoleUser,
			meta.KeyCredential: "",
			meta.KeyUserID:     "",
			meta.KeyUserRole:   meta.ValueUserRoleReader,
			meta.KeySyntax:     meta.ValueSyntaxNone,
		},
		"",






	},

}




















































>
|
<
>
>
>
>
>
|
>
>
>
>

|

|
<
|
|

|
<

|


|
<





|
>
>
>
>
>
>
|
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
682
683
684
685
686
687
688
689
690

691
692
693
694
695
696
697
698
699
700
701
702
703
704

705
706
707
708

709
710
711
712
713

714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}`},


	id.TOCNewTemplateZid: {
		constHeader{
			meta.KeyTitle:  "New Menu",
			meta.KeyRole:   meta.ValueRoleConfiguration,
			meta.KeySyntax: meta.ValueSyntaxZmk,
		},
		`This zettel lists all zettel that should act as a template for new zettel.
These zettel will be included in the ""New"" menu of the WebUI.
* [[New Zettel|00000000090001]]
* [[New User|00000000090002]]`},

	id.TemplateNewZettelZid: {
		constHeader{
			meta.KeyTitle:  "New Zettel",

			meta.KeyRole:   meta.ValueRoleZettel,
			meta.KeySyntax: meta.ValueSyntaxZmk,
		},
		""},


	id.TemplateNewUserZid: {
		constHeader{
			meta.KeyTitle:      "New User",
			meta.KeyRole:       meta.ValueRoleUser,

			meta.KeyCredential: "",
			meta.KeyUserID:     "",
			meta.KeyUserRole:   meta.ValueUserRoleReader,
			meta.KeySyntax:     meta.ValueSyntaxNone,
		},
		""},

	id.DefaultHomeZid: {
		constHeader{
			meta.KeyTitle:  "Home",
			meta.KeyRole:   meta.ValueRoleZettel,
			meta.KeySyntax: meta.ValueSyntaxZmk,
		},
		`=== Thank you for using Zettelstore!

You will find the lastest information about Zettelstore at [[https://zettelstore.de]].
Check that website regulary for [[upgrades|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version.
You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before upgrading.
Sometimes, you have to edit some of your Zettelstore-related zettel before upgrading.
Since Zettelstore is currently in a development state, every upgrade might fix some of your problems.
To check for versions, there is a zettel with the [[current version|00000000000001]] of your Zettelstore.

If you have problems concerning Zettelstore,
do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]].

=== Reporting errors
If you have encountered an error, please include the content of the following zettel in your mail:
* [[Zettelstore Version|00000000000001]]
* [[Zettelstore Operating System|00000000000003]]
* [[Zettelstore Startup Configuration|00000000000096]]
* [[Zettelstore Startup Values|00000000000098]]
* [[Zettelstore Runtime Configuration|00000000000100]]

Additionally, you have to describe, what you have done before that error occurs
and what you have expected instead.
Please do not forget to include the error message, if there is one.

Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"".
Otherwise, only some zettel are linked.
To enable expert mode, edit the zettel [[Zettelstore Runtime Configuration|00000000000100]]:
please set the metadata value of the key ''expert-mode'' to true.
To do you, enter the string ''expert-mode:true'' inside the edit view of the metadata.

=== Information about this zettel
This zettel is your home zettel.
It is part of the Zettelstore software itself.
Every time you click on the [[Home|//]] link in the menu bar, you will be redirected to this zettel.

You can change the content of this zettel by clicking on ""Edit"" above.
This allows you to customize your home zettel.

Alternatively, you can designate another zettel as your home zettel.
Edit the [[Zettelstore Runtime Configuration|00000000000100]] by adding the metadata key ''home-zettel''.
Its value is the identifier of the zettel that should act as the new home zettel.
You will find the identifier of each zettel between their ""Edit"" and the ""Info"" link above.
The identifier of this zettel is ''00010000000000''.
If you provide a wrong identifier, this zettel will be shown as the home zettel.
Take a look inside the manual for further details.
`},
}

Changes to place/constplace/constplace.go.

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
func (cp *constPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	if z, ok := cp.zettel[zid]; ok {
		return makeMeta(zid, z.header), nil
	}
	return nil, place.ErrNotFound
}

func (cp *constPlace) FetchZids(ctx context.Context) (map[id.Zid]bool, error) {
	result := make(map[id.Zid]bool, len(cp.zettel))
	for zid := range cp.zettel {
		result[zid] = true
	}
	return result, nil
}

func (cp *constPlace) SelectMeta(







|
|







73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
func (cp *constPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	if z, ok := cp.zettel[zid]; ok {
		return makeMeta(zid, z.header), nil
	}
	return nil, place.ErrNotFound
}

func (cp *constPlace) FetchZids(ctx context.Context) (id.Set, error) {
	result := id.NewSetCap(len(cp.zettel))
	for zid := range cp.zettel {
		result[zid] = true
	}
	return result, nil
}

func (cp *constPlace) SelectMeta(
122
123
124
125
126
127
128
129
130
131
132
133
134
func (cp *constPlace) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if _, ok := cp.zettel[zid]; ok {
		return place.ErrReadOnly
	}
	return place.ErrNotFound
}

func (cp *constPlace) Reload(ctx context.Context) error { return nil }

func (cp *constPlace) ReadStats(st *place.Stats) {
	st.ReadOnly = true
	st.Zettel = len(cp.zettel)
}







<
<




122
123
124
125
126
127
128


129
130
131
132
func (cp *constPlace) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if _, ok := cp.zettel[zid]; ok {
		return place.ErrReadOnly
	}
	return place.ErrNotFound
}



func (cp *constPlace) ReadStats(st *place.Stats) {
	st.ReadOnly = true
	st.Zettel = len(cp.zettel)
}

Changes to place/dirplace/directory/service.go.

48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

func updateEntry(de *Entry, ev *fileEvent) {
	if ev.ext == "meta" {
		de.MetaSpec = MetaSpecFile
		de.MetaPath = ev.path
		return
	}
	if len(de.ContentExt) != 0 && de.ContentExt != ev.ext {
		de.Duplicates = true
		return
	}
	if de.MetaSpec != MetaSpecFile {
		if ev.ext == "zettel" {
			de.MetaSpec = MetaSpecHeader
		} else {







|







48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

func updateEntry(de *Entry, ev *fileEvent) {
	if ev.ext == "meta" {
		de.MetaSpec = MetaSpecFile
		de.MetaPath = ev.path
		return
	}
	if de.ContentExt != "" && de.ContentExt != ev.ext {
		de.Duplicates = true
		return
	}
	if de.MetaSpec != MetaSpecFile {
		if ev.ext == "zettel" {
			de.MetaSpec = MetaSpecHeader
		} else {
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138


















139
140
141
142
143
144
145
					close(ready)
					ready = nil
				}
				srv.notifyChange(place.OnReload, id.Invalid)
			case fileStatusError:
				log.Println("DIRPLACE", "ERROR", ev.err)
			case fileStatusUpdate:
				if newMap != nil {
					dirMapUpdate(newMap, ev)
				} else {
					dirMapUpdate(curMap, ev)
					srv.notifyChange(place.OnUpdate, ev.zid)
				}
			case fileStatusDelete:
				if newMap != nil {
					deleteFromMap(newMap, ev)
				} else {
					deleteFromMap(curMap, ev)
					srv.notifyChange(place.OnDelete, ev.zid)
				}
			}
		case cmd, ok := <-srv.cmds:
			if ok {
				cmd.run(curMap)
			}
		}
	}
}



















type dirCmd interface {
	run(m dirMap)
}

type cmdNumEntries struct {
	result chan<- resNumEntries







|
<
<
<
<
<

|
<
<
<
<
<








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







111
112
113
114
115
116
117
118





119
120





121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
					close(ready)
					ready = nil
				}
				srv.notifyChange(place.OnReload, id.Invalid)
			case fileStatusError:
				log.Println("DIRPLACE", "ERROR", ev.err)
			case fileStatusUpdate:
				srv.processFileUpdateEvent(ev, curMap, newMap)





			case fileStatusDelete:
				srv.processFileDeleteEvent(ev, curMap, newMap)





			}
		case cmd, ok := <-srv.cmds:
			if ok {
				cmd.run(curMap)
			}
		}
	}
}

func (srv *Service) processFileUpdateEvent(ev *fileEvent, curMap, newMap dirMap) {
	if newMap != nil {
		dirMapUpdate(newMap, ev)
	} else {
		dirMapUpdate(curMap, ev)
		srv.notifyChange(place.OnUpdate, ev.zid)
	}
}

func (srv *Service) processFileDeleteEvent(ev *fileEvent, curMap, newMap dirMap) {
	if newMap != nil {
		deleteFromMap(newMap, ev)
	} else {
		deleteFromMap(curMap, ev)
		srv.notifyChange(place.OnDelete, ev.zid)
	}
}

type dirCmd interface {
	run(m dirMap)
}

type cmdNumEntries struct {
	result chan<- resNumEntries

Changes to place/dirplace/directory/watch.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	"time"

	"github.com/fsnotify/fsnotify"

	"zettelstore.de/z/domain/id"
)

var validFileName = regexp.MustCompile("^(\\d{14}).*(\\.(.+))$")

func matchValidFileName(name string) []string {
	return validFileName.FindStringSubmatch(name)
}

type fileStatus int








|







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	"time"

	"github.com/fsnotify/fsnotify"

	"zettelstore.de/z/domain/id"
)

var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`)

func matchValidFileName(name string) []string {
	return validFileName.FindStringSubmatch(name)
}

type fileStatus int

188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
				}
			case _, ok := <-tick:
				return ok
			}
		}
	}

	pause := func() bool {
		for {
			select {
			case _, ok := <-tick:
				return ok
			}
		}
	}

	for {
		if !reloadFiles() {
			return
		}
		if watcher == nil {
			if !pause() {
				return
			}
		} else {
			if !handleEvents() {
				return
			}
		}







<
<
<
<
<
<
<
<
<





|







188
189
190
191
192
193
194









195
196
197
198
199
200
201
202
203
204
205
206
207
				}
			case _, ok := <-tick:
				return ok
			}
		}
	}










	for {
		if !reloadFiles() {
			return
		}
		if watcher == nil {
			if _, ok := <-tick; !ok {
				return
			}
		} else {
			if !handleEvents() {
				return
			}
		}
228
229
230
231
232
233
234
235



236
237


238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
func addEvent(events []*fileEvent, ev *fileEvent) []*fileEvent {
	switch ev.status {
	case fileStatusNone:
		return events
	case fileStatusReloadStart:
		events = events[0:0]
	case fileStatusUpdate, fileStatusDelete:
		if len(events) == 0 {



			return append(events, ev)
		}


		for i := len(events) - 1; i >= 0; i-- {
			oev := events[i]
			switch oev.status {
			case fileStatusReloadStart, fileStatusReloadEnd:
				return append(events, ev)
			case fileStatusUpdate, fileStatusDelete:
				if ev.path == oev.path {
					if ev.status == oev.status {
						return events
					}
					oev.status = fileStatusNone
					return append(events, ev)
				}
			}
		}
	}
	return append(events, ev)
}

func collectEvents(out chan<- *fileEvent, in <-chan *fileEvent) {
	defer close(out)

	var sendTime time.Time
	sendTimeSet := false







|
>
>
>
|
|
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<
|







219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248

249
250
251
252
253
254
255
256
func addEvent(events []*fileEvent, ev *fileEvent) []*fileEvent {
	switch ev.status {
	case fileStatusNone:
		return events
	case fileStatusReloadStart:
		events = events[0:0]
	case fileStatusUpdate, fileStatusDelete:
		if len(events) > 0 && mergeEvents(events, ev) {
			return events
		}
	}
	return append(events, ev)
}

func mergeEvents(events []*fileEvent, ev *fileEvent) bool {
	for i := len(events) - 1; i >= 0; i-- {
		oev := events[i]
		switch oev.status {
		case fileStatusReloadStart, fileStatusReloadEnd:
			return false
		case fileStatusUpdate, fileStatusDelete:
			if ev.path == oev.path {
				if ev.status == oev.status {
					return true
				}
				oev.status = fileStatusNone
				return false
			}
		}
	}

	return false
}

func collectEvents(out chan<- *fileEvent, in <-chan *fileEvent) {
	defer close(out)

	var sendTime time.Time
	sendTimeSet := false

Changes to place/dirplace/dirplace.go.

177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201

202
203
204
205
206
207
208
	if err != nil {
		return nil, err
	}
	dp.cleanupMeta(ctx, m)
	return m, nil
}

func (dp *dirPlace) FetchZids(ctx context.Context) (map[id.Zid]bool, error) {
	entries := dp.dirSrv.GetEntries()
	result := make(map[id.Zid]bool, len(entries))
	for _, entry := range entries {
		result[entry.Zid] = true
	}
	return result, nil
}

func (dp *dirPlace) SelectMeta(
	ctx context.Context, f *place.Filter, s *place.Sorter) (res []*meta.Meta, err error) {

	hasMatch := place.CreateFilterFunc(f)
	entries := dp.dirSrv.GetEntries()
	res = make([]*meta.Meta, 0, len(entries))
	for _, entry := range entries {
		// TODO: execute requests in parallel
		m, err := getMeta(dp, &entry, entry.Zid)

		if err != nil {
			continue
		}
		dp.cleanupMeta(ctx, m)
		dp.cdata.Filter.Enrich(ctx, m)

		if hasMatch(m) {







|

|














|
>







177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
	if err != nil {
		return nil, err
	}
	dp.cleanupMeta(ctx, m)
	return m, nil
}

func (dp *dirPlace) FetchZids(ctx context.Context) (id.Set, error) {
	entries := dp.dirSrv.GetEntries()
	result := id.NewSetCap(len(entries))
	for _, entry := range entries {
		result[entry.Zid] = true
	}
	return result, nil
}

func (dp *dirPlace) SelectMeta(
	ctx context.Context, f *place.Filter, s *place.Sorter) (res []*meta.Meta, err error) {

	hasMatch := place.CreateFilterFunc(f)
	entries := dp.dirSrv.GetEntries()
	res = make([]*meta.Meta, 0, len(entries))
	for _, entry := range entries {
		// TODO: execute requests in parallel
		m, err1 := getMeta(dp, &entry, entry.Zid)
		err = err1
		if err != nil {
			continue
		}
		dp.cleanupMeta(ctx, m)
		dp.cdata.Filter.Enrich(ctx, m)

		if hasMatch(m) {
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
	oldMeta.Zid = newZid
	newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)}
	if err := setZettel(dp, &newEntry, newZettel); err != nil {
		// "Rollback" rename. No error checking...
		dp.dirSrv.RenameEntry(&newEntry, &curEntry)
		return err
	}
	if err := deleteZettel(dp, &curEntry, curZid); err != nil {
		return err
	}
	return nil
}

func (dp *dirPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
	if dp.readonly {
		return false
	}
	entry := dp.dirSrv.GetEntry(zid)







|
<
<
<







310
311
312
313
314
315
316
317



318
319
320
321
322
323
324
	oldMeta.Zid = newZid
	newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)}
	if err := setZettel(dp, &newEntry, newZettel); err != nil {
		// "Rollback" rename. No error checking...
		dp.dirSrv.RenameEntry(&newEntry, &curEntry)
		return err
	}
	return deleteZettel(dp, &curEntry, curZid)



}

func (dp *dirPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
	if dp.readonly {
		return false
	}
	entry := dp.dirSrv.GetEntry(zid)
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357

	entry := dp.dirSrv.GetEntry(zid)
	if !entry.IsValid() {
		return nil
	}
	dp.dirSrv.DeleteEntry(zid)
	err := deleteZettel(dp, &entry, zid)
	return err
}

func (dp *dirPlace) Reload(ctx context.Context) error {
	// Brute force: stop everything, then start everything.
	// Could be done better in the future...
	err := dp.Stop(ctx)
	if err == nil {
		err = dp.Start(ctx)
	}
	return err
}

func (dp *dirPlace) ReadStats(st *place.Stats) {
	st.ReadOnly = dp.readonly
	st.Zettel = dp.dirSrv.NumEntries()
}







<
<
<
<
<
<
<
<
<
<







332
333
334
335
336
337
338










339
340
341
342
343
344
345

	entry := dp.dirSrv.GetEntry(zid)
	if !entry.IsValid() {
		return nil
	}
	dp.dirSrv.DeleteEntry(zid)
	err := deleteZettel(dp, &entry, zid)










	return err
}

func (dp *dirPlace) ReadStats(st *place.Stats) {
	st.ReadOnly = dp.readonly
	st.Zettel = dp.dirSrv.NumEntries()
}

Changes to place/dirplace/service.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
}

// COMMAND: getMeta ----------------------------------------
//
// Retrieves the meta data from a zettel.

func getMeta(dp *dirPlace, entry *directory.Entry, zid id.Zid) (*meta.Meta, error) {
	rc := make(chan resGetMetaContent)
	dp.getFileChan(zid) <- &fileGetMetaContent{entry, rc}
	res := <-rc
	close(rc)
	return res.meta, res.err
}

type fileGetMeta struct {
	entry *directory.Entry







|
|







33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
}

// COMMAND: getMeta ----------------------------------------
//
// Retrieves the meta data from a zettel.

func getMeta(dp *dirPlace, entry *directory.Entry, zid id.Zid) (*meta.Meta, error) {
	rc := make(chan resGetMeta)
	dp.getFileChan(zid) <- &fileGetMeta{entry, rc}
	res := <-rc
	close(rc)
	return res.meta, res.err
}

type fileGetMeta struct {
	entry *directory.Entry
96
97
98
99
100
101
102

103

104
105
106
107
108
109
110
	var m *meta.Meta
	var content string
	var err error

	switch cmd.entry.MetaSpec {
	case directory.MetaSpecFile:
		m, err = parseMetaFile(cmd.entry.Zid, cmd.entry.MetaPath)

		content, err = readFileContent(cmd.entry.ContentPath)

	case directory.MetaSpecHeader:
		m, content, err = parseMetaContentFile(cmd.entry.Zid, cmd.entry.ContentPath)
	default:
		m = cmd.entry.CalcDefaultMeta()
		content, err = readFileContent(cmd.entry.ContentPath)
	}
	if err == nil {







>
|
>







96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
	var m *meta.Meta
	var content string
	var err error

	switch cmd.entry.MetaSpec {
	case directory.MetaSpecFile:
		m, err = parseMetaFile(cmd.entry.Zid, cmd.entry.MetaPath)
		if err == nil {
			content, err = readFileContent(cmd.entry.ContentPath)
		}
	case directory.MetaSpecHeader:
		m, content, err = parseMetaContentFile(cmd.entry.Zid, cmd.entry.ContentPath)
	default:
		m = cmd.entry.CalcDefaultMeta()
		content, err = readFileContent(cmd.entry.ContentPath)
	}
	if err == nil {
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181


































182
183
184
185
186
187
188
	entry  *directory.Entry
	zettel domain.Zettel
	rc     chan<- resSetZettel
}
type resSetZettel = error

func (cmd *fileSetZettel) run() {
	var f *os.File
	var err error

	switch cmd.entry.MetaSpec {
	case directory.MetaSpecFile:
		f, err = openFileWrite(cmd.entry.MetaPath)
		if err == nil {
			err = writeFileZid(f, cmd.zettel.Meta.Zid)
			if err == nil {
				_, err = cmd.zettel.Meta.Write(f, true)
				if err1 := f.Close(); err == nil {
					err = err1
				}

				if err == nil {
					err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString())
				}
			}
		}

	case directory.MetaSpecHeader:
		f, err = openFileWrite(cmd.entry.ContentPath)
		if err == nil {
			err = writeFileZid(f, cmd.zettel.Meta.Zid)
			if err == nil {
				_, err = cmd.zettel.Meta.WriteAsHeader(f, true)
				if err == nil {
					_, err = f.WriteString(cmd.zettel.Content.AsString())
					if err1 := f.Close(); err == nil {
						err = err1
					}
				}
			}
		}

	case directory.MetaSpecNone:
		// TODO: if meta has some additional infos: write meta to new .meta;
		// update entry in dir

		err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString())

	case directory.MetaSpecUnknown:
		panic("TODO: ???")
	}
	cmd.rc <- err
}



































// COMMAND: deleteZettel ----------------------------------------
//
// Deletes an existing zettel.

func deleteZettel(dp *dirPlace, entry *directory.Entry, zid id.Zid) error {
	rc := make(chan resDeleteZettel)







<

<


<
<
<
<
<
<
|
<
<
<
<
<
<
<
<

<
<
<
<
<
<
<
<
|
<
<
<
<
<



<

<





>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







131
132
133
134
135
136
137

138

139
140






141








142








143





144
145
146

147

148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
	entry  *directory.Entry
	zettel domain.Zettel
	rc     chan<- resSetZettel
}
type resSetZettel = error

func (cmd *fileSetZettel) run() {

	var err error

	switch cmd.entry.MetaSpec {
	case directory.MetaSpecFile:






		err = cmd.runMetaSpecFile()








	case directory.MetaSpecHeader:








		err = cmd.runMetaSpecHeader()





	case directory.MetaSpecNone:
		// TODO: if meta has some additional infos: write meta to new .meta;
		// update entry in dir

		err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString())

	case directory.MetaSpecUnknown:
		panic("TODO: ???")
	}
	cmd.rc <- err
}

func (cmd *fileSetZettel) runMetaSpecFile() error {
	f, err := openFileWrite(cmd.entry.MetaPath)
	if err == nil {
		err = writeFileZid(f, cmd.zettel.Meta.Zid)
		if err == nil {
			_, err = cmd.zettel.Meta.Write(f, true)
			if err1 := f.Close(); err == nil {
				err = err1
			}
			if err == nil {
				err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString())
			}
		}
	}
	return err
}

func (cmd *fileSetZettel) runMetaSpecHeader() error {
	f, err := openFileWrite(cmd.entry.ContentPath)
	if err == nil {
		err = writeFileZid(f, cmd.zettel.Meta.Zid)
		if err == nil {
			_, err = cmd.zettel.Meta.WriteAsHeader(f, true)
			if err == nil {
				_, err = f.WriteString(cmd.zettel.Content.AsString())
				if err1 := f.Close(); err == nil {
					err = err1
				}
			}
		}
	}
	return err
}

// COMMAND: deleteZettel ----------------------------------------
//
// Deletes an existing zettel.

func deleteZettel(dp *dirPlace, entry *directory.Entry, zid id.Zid) error {
	rc := make(chan resDeleteZettel)
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
}

func cleanupMeta(m *meta.Meta, entry *directory.Entry) {
	if title, ok := m.Get(meta.KeyTitle); !ok || title == "" {
		m.Set(meta.KeyTitle, entry.Zid.String())
	}

	switch entry.MetaSpec {
	case directory.MetaSpecFile:
		if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
			dm := entry.CalcDefaultMeta()
			syntax, ok = dm.Get(meta.KeySyntax)
			if !ok {
				panic("Default meta must contain syntax")
			}
			m.Set(meta.KeySyntax, syntax)
		}
	}

	if entry.Duplicates {
		m.Set(meta.KeyDuplicates, meta.ValueTrue)
	}
}

func openFileWrite(path string) (*os.File, error) {
	return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
}

func writeFileZid(f *os.File, zid id.Zid) error {
	_, err := f.WriteString("id: ")
	if err == nil {
		_, err = f.Write(zid.Bytes())
		if err == nil {







<
|
















|







253
254
255
256
257
258
259

260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
}

func cleanupMeta(m *meta.Meta, entry *directory.Entry) {
	if title, ok := m.Get(meta.KeyTitle); !ok || title == "" {
		m.Set(meta.KeyTitle, entry.Zid.String())
	}


	if entry.MetaSpec == directory.MetaSpecFile {
		if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
			dm := entry.CalcDefaultMeta()
			syntax, ok = dm.Get(meta.KeySyntax)
			if !ok {
				panic("Default meta must contain syntax")
			}
			m.Set(meta.KeySyntax, syntax)
		}
	}

	if entry.Duplicates {
		m.Set(meta.KeyDuplicates, meta.ValueTrue)
	}
}

func openFileWrite(path string) (*os.File, error) {
	return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
}

func writeFileZid(f *os.File, zid id.Zid) error {
	_, err := f.WriteString("id: ")
	if err == nil {
		_, err = f.Write(zid.Bytes())
		if err == nil {

Changes to place/filter.go.

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// FilterFunc is a predicate to check if given meta must be selected.
type FilterFunc func(*meta.Meta) bool

func selectAll(m *meta.Meta) bool { return true }

type matchFunc func(value string) bool

func matchAlways(value string) bool { return true }
func matchNever(value string) bool  { return false }

type matchSpec struct {
	key   string
	match matchFunc
}

// CreateFilterFunc calculates a filter func based on the given filter.
func CreateFilterFunc(filter *Filter) FilterFunc {
	if filter == nil {
		return selectAll
	}

	specs := make([]matchSpec, 0, len(filter.Expr))
	var searchAll FilterFunc
	for key, values := range filter.Expr {
		if len(key) == 0 {
			// Special handling if searching all keys...
			searchAll = createSearchAllFunc(values, filter.Negate)
			continue
		}
		if meta.KeyIsValid(key) {
			match := createMatchFunc(key, values)
			if match != nil {
				specs = append(specs, matchSpec{key, match})
			}
		}
	}
	if len(specs) == 0 {
		if searchAll == nil {
			if sel := filter.Select; sel != nil {
				return sel
			}
			return selectAll
		}







<
|











|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







29
30
31
32
33
34
35

36
37
38
39
40
41
42
43
44
45
46
47
48















49
50
51
52
53
54
55
// FilterFunc is a predicate to check if given meta must be selected.
type FilterFunc func(*meta.Meta) bool

func selectAll(m *meta.Meta) bool { return true }

type matchFunc func(value string) bool


func matchNever(value string) bool { return false }

type matchSpec struct {
	key   string
	match matchFunc
}

// CreateFilterFunc calculates a filter func based on the given filter.
func CreateFilterFunc(filter *Filter) FilterFunc {
	if filter == nil {
		return selectAll
	}
	specs, searchAll := createFilterSpecs(filter)















	if len(specs) == 0 {
		if searchAll == nil {
			if sel := filter.Select; sel != nil {
				return sel
			}
			return selectAll
		}
84
85
86
87
88
89
90



















91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122


















123












124
125
126
127
128
129
130

131

132
133
134
135
136
137
138
139
140
141
142
143

144

145
146
147




148
149
150
151
152
153
154
155
156

157

158
159
160
161
162
163
164
165
166
167

168

169
170
171
172
173
174
175
176
177
178
179
180
181
182

183
184
185
186
187
188
189
	if searchAll == nil {
		return addSelectFunc(filter, searchMeta)
	}
	return addSelectFunc(filter, func(meta *meta.Meta) bool {
		return searchAll(meta) || searchMeta(meta)
	})
}




















func addSelectFunc(filter *Filter, f FilterFunc) FilterFunc {
	if filter == nil {
		return f
	}
	if sel := filter.Select; sel != nil {
		return func(meta *meta.Meta) bool {
			return sel(meta) && f(meta)
		}
	}
	return f
}

func createMatchFunc(key string, values []string) matchFunc {
	switch meta.Type(key) {
	case meta.TypeBool:
		preValues := make([]bool, 0, len(values))
		for _, v := range values {
			preValues = append(preValues, meta.BoolValue(v))
		}
		return func(value string) bool {
			bValue := meta.BoolValue(value)
			for _, v := range preValues {
				if bValue != v {
					return false
				}
			}
			return true
		}
	case meta.TypeCredential:
		return matchNever
	case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout


















		return func(value string) bool {












			for _, v := range values {
				if !strings.HasPrefix(value, v) {
					return false
				}
			}
			return true
		}

	case meta.TypeIDSet:

		idValues := preprocessSet(sliceToLower(values))
		return func(value string) bool {
			ids := meta.ListFromValue(value)
			for _, neededIDs := range idValues {
				for _, neededID := range neededIDs {
					if !matchAllID(ids, neededID) {
						return false
					}
				}
			}
			return true
		}

	case meta.TypeTagSet:

		tagValues := preprocessSet(values)
		return func(value string) bool {
			tags := meta.ListFromValue(value)




			for _, neededTags := range tagValues {
				for _, neededTag := range neededTags {
					if !matchAllTag(tags, neededTag) {
						return false
					}
				}
			}
			return true
		}

	case meta.TypeWord:

		values = sliceToLower(values)
		return func(value string) bool {
			value = strings.ToLower(value)
			for _, v := range values {
				if value != v {
					return false
				}
			}
			return true
		}

	case meta.TypeWordSet:

		wordValues := preprocessSet(sliceToLower(values))
		return func(value string) bool {
			words := meta.ListFromValue(value)
			for _, neededWords := range wordValues {
				for _, neededWord := range neededWords {
					if !matchAllWord(words, neededWord) {
						return false
					}
				}
			}
			return true
		}
	}


	values = sliceToLower(values)
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, v := range values {
			if !strings.Contains(value, v) {
				return false
			}







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
















<
<
<
<
<
<
<
<
<
<
<
|
<



>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
|
|
|
>
|
>
|
|
|
|
|
|
|
|
|
|
|
|
>
|
>
|
|
|
>
>
>
>
|
|
|
|
|
|
|
|
|
>
|
>
|
|
|
|
|
|
|
|
|
|
>
|
>
|
|
|
|
|
|
|
|
|
|
|
|
|

>







68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109











110

111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
	if searchAll == nil {
		return addSelectFunc(filter, searchMeta)
	}
	return addSelectFunc(filter, func(meta *meta.Meta) bool {
		return searchAll(meta) || searchMeta(meta)
	})
}

func createFilterSpecs(filter *Filter) ([]matchSpec, FilterFunc) {
	specs := make([]matchSpec, 0, len(filter.Expr))
	var searchAll FilterFunc
	for key, values := range filter.Expr {
		if key == "" {
			// Special handling if searching all keys...
			searchAll = createSearchAllFunc(values, filter.Negate)
			continue
		}
		if meta.KeyIsValid(key) {
			match := createMatchFunc(key, values)
			if match != nil {
				specs = append(specs, matchSpec{key, match})
			}
		}
	}
	return specs, searchAll
}

func addSelectFunc(filter *Filter, f FilterFunc) FilterFunc {
	if filter == nil {
		return f
	}
	if sel := filter.Select; sel != nil {
		return func(meta *meta.Meta) bool {
			return sel(meta) && f(meta)
		}
	}
	return f
}

func createMatchFunc(key string, values []string) matchFunc {
	switch meta.Type(key) {
	case meta.TypeBool:











		return createMatchBoolFunc(values)

	case meta.TypeCredential:
		return matchNever
	case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout
		return createMatchIDFunc(values)
	case meta.TypeIDSet:
		return createMatchIDSetFunc(values)
	case meta.TypeTagSet:
		return createMatchTagSetFunc(values)
	case meta.TypeWord:
		return createMatchWordFunc(values)
	case meta.TypeWordSet:
		return createMatchWordSetFunc(values)
	}
	return createMatchStringFunc(values)
}

func createMatchBoolFunc(values []string) matchFunc {
	preValues := make([]bool, 0, len(values))
	for _, v := range values {
		preValues = append(preValues, meta.BoolValue(v))
	}
	return func(value string) bool {
		bValue := meta.BoolValue(value)
		for _, v := range preValues {
			if bValue != v {
				return false
			}
		}
		return true
	}
}

func createMatchIDFunc(values []string) matchFunc {
	return func(value string) bool {
		for _, v := range values {
			if !strings.HasPrefix(value, v) {
				return false
			}
		}
		return true
	}
}

func createMatchIDSetFunc(values []string) matchFunc {
	idValues := preprocessSet(sliceToLower(values))
	return func(value string) bool {
		ids := meta.ListFromValue(value)
		for _, neededIDs := range idValues {
			for _, neededID := range neededIDs {
				if !matchAllID(ids, neededID) {
					return false
				}
			}
		}
		return true
	}
}

func createMatchTagSetFunc(values []string) matchFunc {
	tagValues := preprocessSet(values)
	return func(value string) bool {
		tags := meta.ListFromValue(value)
		// Remove leading '#' from each tag
		for i, tag := range tags {
			tags[i] = meta.CleanTag(tag)
		}
		for _, neededTags := range tagValues {
			for _, neededTag := range neededTags {
				if !matchAllTag(tags, neededTag) {
					return false
				}
			}
		}
		return true
	}
}

func createMatchWordFunc(values []string) matchFunc {
	values = sliceToLower(values)
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, v := range values {
			if value != v {
				return false
			}
		}
		return true
	}
}

func createMatchWordSetFunc(values []string) matchFunc {
	wordValues := preprocessSet(sliceToLower(values))
	return func(value string) bool {
		words := meta.ListFromValue(value)
		for _, neededWords := range wordValues {
			for _, neededWord := range neededWords {
				if !matchAllWord(words, neededWord) {
					return false
				}
			}
		}
		return true
	}
}

func createMatchStringFunc(values []string) matchFunc {
	values = sliceToLower(values)
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, v := range values {
			if !strings.Contains(value, v) {
				return false
			}
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
	result := make([]string, 0, len(sl))
	for _, s := range sl {
		result = append(result, strings.ToLower(s))
	}
	return result
}

func isEmptySlice(sl []string) bool {
	for _, s := range sl {
		if len(s) > 0 {
			return false
		}
	}
	return true
}

func preprocessSet(set []string) [][]string {
	result := make([][]string, 0, len(set))
	for _, elem := range set {
		splitElems := strings.Split(elem, ",")
		valueElems := make([]string, 0, len(splitElems))
		for _, se := range splitElems {
			e := strings.TrimSpace(se)







<
<
<
<
<
<
<
<
<







268
269
270
271
272
273
274









275
276
277
278
279
280
281
	result := make([]string, 0, len(sl))
	for _, s := range sl {
		result = append(result, strings.ToLower(s))
	}
	return result
}










func preprocessSet(set []string) [][]string {
	result := make([][]string, 0, len(set))
	for _, elem := range set {
		splitElems := strings.Split(elem, ",")
		valueElems := make([]string, 0, len(splitElems))
		for _, se := range splitElems {
			e := strings.TrimSpace(se)

Changes to place/manager/manager.go.

87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
	return result
}

// Manager is a coordinating place.
type Manager struct {
	mx         sync.RWMutex
	started    bool
	placeURIs  []url.URL
	subplaces  []place.Place
	filter     index.MetaFilter
	observers  []func(place.ChangeInfo)
	mxObserver sync.RWMutex
	done       chan struct{}
	infos      chan place.ChangeInfo
}







<







87
88
89
90
91
92
93

94
95
96
97
98
99
100
	return result
}

// Manager is a coordinating place.
type Manager struct {
	mx         sync.RWMutex
	started    bool

	subplaces  []place.Place
	filter     index.MetaFilter
	observers  []func(place.ChangeInfo)
	mxObserver sync.RWMutex
	done       chan struct{}
	infos      chan place.ChangeInfo
}
189
190
191
192
193
194
195
196



197



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
func (mgr *Manager) Start(ctx context.Context) error {
	mgr.mx.Lock()
	if mgr.started {
		mgr.mx.Unlock()
		return place.ErrStarted
	}
	for i := len(mgr.subplaces) - 1; i >= 0; i-- {
		if ssi, ok := mgr.subplaces[i].(place.StartStopper); ok {



			if err := ssi.Start(ctx); err != nil {



				for j := i + 1; j < len(mgr.subplaces); j++ {
					if ssj, ok := mgr.subplaces[j].(place.StartStopper); ok {
						ssj.Stop(ctx)
					}
				}
				mgr.mx.Unlock()
				return err
			}
		}
	}
	mgr.done = make(chan struct{})
	go notifier(mgr.notifyObserver, mgr.infos, mgr.done)
	mgr.started = true
	mgr.mx.Unlock()
	mgr.infos <- place.ChangeInfo{Reason: place.OnReload, Zid: id.Invalid}
	return nil







|
>
>
>
|
>
>
>
|
|
|
|
|
|
|
<
<







188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209


210
211
212
213
214
215
216
func (mgr *Manager) Start(ctx context.Context) error {
	mgr.mx.Lock()
	if mgr.started {
		mgr.mx.Unlock()
		return place.ErrStarted
	}
	for i := len(mgr.subplaces) - 1; i >= 0; i-- {
		ssi, ok := mgr.subplaces[i].(place.StartStopper)
		if !ok {
			continue
		}
		err := ssi.Start(ctx)
		if err == nil {
			continue
		}
		for j := i + 1; j < len(mgr.subplaces); j++ {
			if ssj, ok := mgr.subplaces[j].(place.StartStopper); ok {
				ssj.Stop(ctx)
			}
		}
		mgr.mx.Unlock()
		return err


	}
	mgr.done = make(chan struct{})
	go notifier(mgr.notifyObserver, mgr.infos, mgr.done)
	mgr.started = true
	mgr.mx.Unlock()
	mgr.infos <- place.ChangeInfo{Reason: place.OnReload, Zid: id.Invalid}
	return nil
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
			return m, err
		}
	}
	return nil, place.ErrNotFound
}

// FetchZids returns the set of all zettel identifer managed by the place.
func (mgr *Manager) FetchZids(ctx context.Context) (result map[id.Zid]bool, err error) {
	mgr.mx.RLock()
	defer mgr.mx.RUnlock()
	if !mgr.started {
		return nil, place.ErrStopped
	}
	for _, p := range mgr.subplaces {
		zids, err := p.FetchZids(ctx)







|







283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
			return m, err
		}
	}
	return nil, place.ErrNotFound
}

// FetchZids returns the set of all zettel identifer managed by the place.
func (mgr *Manager) FetchZids(ctx context.Context) (result id.Set, err error) {
	mgr.mx.RLock()
	defer mgr.mx.RUnlock()
	if !mgr.started {
		return nil, place.ErrStopped
	}
	for _, p := range mgr.subplaces {
		zids, err := p.FetchZids(ctx)
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
		if err := p.DeleteZettel(ctx, zid); err != place.ErrNotFound && err != place.ErrReadOnly {
			return err
		}
	}
	return place.ErrNotFound
}

// Reload clears all caches, reloads all internal data to reflect changes
// that were possibly undetected.
func (mgr *Manager) Reload(ctx context.Context) error {
	mgr.mx.RLock()
	defer mgr.mx.RUnlock()
	if !mgr.started {
		return place.ErrStopped
	}
	var err error
	for _, p := range mgr.subplaces {
		if err1 := p.Reload(ctx); err1 != nil && err == nil {
			err = err1
		}
	}
	mgr.infos <- place.ChangeInfo{Reason: place.OnReload, Zid: id.Invalid}
	return err
}

// ReadStats populates st with place statistics
func (mgr *Manager) ReadStats(st *place.Stats) {
	subStats := make([]place.Stats, len(mgr.subplaces))
	for i, p := range mgr.subplaces {
		p.ReadStats(&subStats[i])
	}








<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







418
419
420
421
422
423
424


















425
426
427
428
429
430
431
		if err := p.DeleteZettel(ctx, zid); err != place.ErrNotFound && err != place.ErrReadOnly {
			return err
		}
	}
	return place.ErrNotFound
}



















// ReadStats populates st with place statistics
func (mgr *Manager) ReadStats(st *place.Stats) {
	subStats := make([]place.Stats, len(mgr.subplaces))
	for i, p := range mgr.subplaces {
		p.ReadStats(&subStats[i])
	}

Changes to place/memplace/memplace.go.

107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
	mp.mx.RUnlock()
	if !ok {
		return nil, place.ErrNotFound
	}
	return zettel.Meta.Clone(), nil
}

func (mp *memPlace) FetchZids(ctx context.Context) (map[id.Zid]bool, error) {
	mp.mx.RLock()
	result := make(map[id.Zid]bool, len(mp.zettel))
	for zid := range mp.zettel {
		result[zid] = true
	}
	mp.mx.RUnlock()
	return result, nil
}








|

|







107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
	mp.mx.RUnlock()
	if !ok {
		return nil, place.ErrNotFound
	}
	return zettel.Meta.Clone(), nil
}

func (mp *memPlace) FetchZids(ctx context.Context) (id.Set, error) {
	mp.mx.RLock()
	result := id.NewSetCap(len(mp.zettel))
	for zid := range mp.zettel {
		result[zid] = true
	}
	mp.mx.RUnlock()
	return result, nil
}

195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
	}
	delete(mp.zettel, zid)
	mp.mx.Unlock()
	mp.notifyChanged(place.OnDelete, zid)
	return nil
}

func (mp *memPlace) Reload(ctx context.Context) error { return nil }

func (mp *memPlace) ReadStats(st *place.Stats) {
	st.ReadOnly = false
	mp.mx.RLock()
	st.Zettel = len(mp.zettel)
	mp.mx.RUnlock()
}







<
<






195
196
197
198
199
200
201


202
203
204
205
206
207
	}
	delete(mp.zettel, zid)
	mp.mx.Unlock()
	mp.notifyChanged(place.OnDelete, zid)
	return nil
}



func (mp *memPlace) ReadStats(st *place.Stats) {
	st.ReadOnly = false
	mp.mx.RLock()
	st.Zettel = len(mp.zettel)
	mp.mx.RUnlock()
}

Changes to place/place.go.

37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)

	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)

	// FetchZids returns the set of all zettel identifer managed by the place.
	FetchZids(ctx context.Context) (map[id.Zid]bool, error)

	// SelectMeta returns all zettel meta data that match the selection criteria.
	// TODO: more docs
	SelectMeta(ctx context.Context, f *Filter, s *Sorter) ([]*meta.Meta, error)

	// CanUpdateZettel returns true, if place could possibly update the given zettel.
	CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool







|







37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)

	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)

	// FetchZids returns the set of all zettel identifer managed by the place.
	FetchZids(ctx context.Context) (id.Set, error)

	// SelectMeta returns all zettel meta data that match the selection criteria.
	// TODO: more docs
	SelectMeta(ctx context.Context, f *Filter, s *Sorter) ([]*meta.Meta, error)

	// CanUpdateZettel returns true, if place could possibly update the given zettel.
	CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

	// CanDeleteZettel returns true, if place could possibly delete the given zettel.
	CanDeleteZettel(ctx context.Context, zid id.Zid) bool

	// DeleteZettel removes the zettel from the place.
	DeleteZettel(ctx context.Context, zid id.Zid) error

	// Reload clears all caches, reloads all internal data to reflect changes
	// that were possibly undetected.
	Reload(ctx context.Context) error

	// ReadStats populates st with place statistics
	ReadStats(st *Stats)
}

// Stats records statistics about the place.
type Stats struct {
	// ReadOnly indicates that the places cannot be changed







<
<
<
<







61
62
63
64
65
66
67




68
69
70
71
72
73
74

	// CanDeleteZettel returns true, if place could possibly delete the given zettel.
	CanDeleteZettel(ctx context.Context, zid id.Zid) bool

	// DeleteZettel removes the zettel from the place.
	DeleteZettel(ctx context.Context, zid id.Zid) error





	// ReadStats populates st with place statistics
	ReadStats(st *Stats)
}

// Stats records statistics about the place.
type Stats struct {
	// ReadOnly indicates that the places cannot be changed
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
	}
}

func (err *ErrNotAllowed) Error() string {
	if err.User == nil {
		if err.Zid.IsValid() {
			return fmt.Sprintf(
				"Operation %q on zettel %v not allowed for not authorized user",
				err.Op,
				err.Zid.String())
		}
		return fmt.Sprintf("Operation %q not allowed for not authorized user", err.Op)
	}
	if err.Zid.IsValid() {
		return fmt.Sprintf(
			"Operation %q on zettel %v not allowed for user %v/%v",
			err.Op,
			err.Zid.String(),
			err.User.GetDefault(meta.KeyUserID, "?"),
			err.User.Zid.String())
	}
	return fmt.Sprintf(
		"Operation %q not allowed for user %v/%v",
		err.Op,
		err.User.GetDefault(meta.KeyUserID, "?"),
		err.User.Zid.String())
}

// IsErrNotAllowed return true, if the error is of type ErrNotAllowed.
func IsErrNotAllowed(err error) bool {
	_, ok := err.(*ErrNotAllowed)
	return ok
}

// ErrStarted is returned when trying to start an already started place.
var ErrStarted = errors.New("Place is already started")

// ErrStopped is returned if calling methods on a place that was not started.
var ErrStopped = errors.New("Place is stopped")

// ErrReadOnly is returned if there is an attepmt to write to a read-only place.
var ErrReadOnly = errors.New("Read-only place")

// ErrNotFound is returned if a zettel was not found in the place.
var ErrNotFound = errors.New("Zettel not found")

// ErrInvalidID is returned if the zettel id is not appropriate for the place operation.
type ErrInvalidID struct{ Zid id.Zid }

func (err *ErrInvalidID) Error() string { return "Invalid Zettel id: " + err.Zid.String() }

// Filter specifies a mechanism for selecting zettel.
type Filter struct {
	Expr   FilterExpr
	Negate bool
	Select func(*meta.Meta) bool
}







|



|



|






|












|


|


|


|




|







134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
	}
}

func (err *ErrNotAllowed) Error() string {
	if err.User == nil {
		if err.Zid.IsValid() {
			return fmt.Sprintf(
				"operation %q on zettel %v not allowed for not authorized user",
				err.Op,
				err.Zid.String())
		}
		return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op)
	}
	if err.Zid.IsValid() {
		return fmt.Sprintf(
			"operation %q on zettel %v not allowed for user %v/%v",
			err.Op,
			err.Zid.String(),
			err.User.GetDefault(meta.KeyUserID, "?"),
			err.User.Zid.String())
	}
	return fmt.Sprintf(
		"operation %q not allowed for user %v/%v",
		err.Op,
		err.User.GetDefault(meta.KeyUserID, "?"),
		err.User.Zid.String())
}

// IsErrNotAllowed return true, if the error is of type ErrNotAllowed.
func IsErrNotAllowed(err error) bool {
	_, ok := err.(*ErrNotAllowed)
	return ok
}

// ErrStarted is returned when trying to start an already started place.
var ErrStarted = errors.New("place is already started")

// ErrStopped is returned if calling methods on a place that was not started.
var ErrStopped = errors.New("place is stopped")

// ErrReadOnly is returned if there is an attepmt to write to a read-only place.
var ErrReadOnly = errors.New("read-only place")

// ErrNotFound is returned if a zettel was not found in the place.
var ErrNotFound = errors.New("zettel not found")

// ErrInvalidID is returned if the zettel id is not appropriate for the place operation.
type ErrInvalidID struct{ Zid id.Zid }

func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() }

// Filter specifies a mechanism for selecting zettel.
type Filter struct {
	Expr   FilterExpr
	Negate bool
	Select func(*meta.Meta) bool
}

Changes to place/progplace/progplace.go.

118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
				return m, nil
			}
		}
	}
	return nil, place.ErrNotFound
}

func (pp *progPlace) FetchZids(ctx context.Context) (map[id.Zid]bool, error) {
	result := make(map[id.Zid]bool, len(pp.zettel))
	for zid, gen := range pp.zettel {
		if genMeta := gen.meta; genMeta != nil {
			if genMeta(zid) != nil {
				result[zid] = true
			}
		}
	}







|
|







118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
				return m, nil
			}
		}
	}
	return nil, place.ErrNotFound
}

func (pp *progPlace) FetchZids(ctx context.Context) (id.Set, error) {
	result := id.NewSetCap(len(pp.zettel))
	for zid, gen := range pp.zettel {
		if genMeta := gen.meta; genMeta != nil {
			if genMeta(zid) != nil {
				result[zid] = true
			}
		}
	}
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
func (pp *progPlace) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if _, ok := pp.zettel[zid]; ok {
		return place.ErrReadOnly
	}
	return place.ErrNotFound
}

func (pp *progPlace) Reload(ctx context.Context) error { return nil }

func (pp *progPlace) ReadStats(st *place.Stats) {
	st.ReadOnly = true
	st.Zettel = len(pp.zettel)
}

func updateMeta(m *meta.Meta) {
	m.Set(meta.KeySyntax, meta.ValueSyntaxZmk)
	m.Set(meta.KeyRole, meta.ValueRoleConfiguration)
	m.Set(meta.KeyReadOnly, meta.ValueTrue)
	if _, ok := m.Get(meta.KeyVisibility); !ok {
		m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert)
	}
}







<
<













176
177
178
179
180
181
182


183
184
185
186
187
188
189
190
191
192
193
194
195
func (pp *progPlace) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if _, ok := pp.zettel[zid]; ok {
		return place.ErrReadOnly
	}
	return place.ErrNotFound
}



func (pp *progPlace) ReadStats(st *place.Stats) {
	st.ReadOnly = true
	st.Zettel = len(pp.zettel)
}

func updateMeta(m *meta.Meta) {
	m.Set(meta.KeySyntax, meta.ValueSyntaxZmk)
	m.Set(meta.KeyRole, meta.ValueRoleConfiguration)
	m.Set(meta.KeyReadOnly, meta.ValueTrue)
	if _, ok := m.Get(meta.KeyVisibility); !ok {
		m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert)
	}
}

Changes to place/sorter.go.

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

79









80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

96

97
98
99
100
101
102
103
104
105
106
107
108
109
110

111
112
113
114
115
116
117
			func(i, j int) bool {
				return metaList[i].Zid > metaList[j].Zid
			})
		return metaList
	}

	if s.Order == "" {
		sort.Slice(metaList, getSortFunc(meta.KeyID, true, metaList))
	} else if s.Order == RandomOrder {
		rand.Shuffle(len(metaList), func(i, j int) {
			metaList[i], metaList[j] = metaList[j], metaList[i]
		})
	} else {
		sort.Slice(metaList, getSortFunc(s.Order, s.Descending, metaList))
	}

	if s.Offset > 0 {
		if s.Offset > len(metaList) {
			return nil
		}
		metaList = metaList[s.Offset:]
	}
	if s.Limit > 0 && s.Limit < len(metaList) {
		metaList = metaList[:s.Limit]
	}
	return metaList
}

type sortFunc func(i, j int) bool

func getSortFunc(key string, descending bool, ml []*meta.Meta) sortFunc {
	keyType := meta.Type(key)
	if key == meta.KeyID || keyType == meta.TypeCredential {
		if descending {
			return func(i, j int) bool { return ml[i].Zid > ml[j].Zid }
		}
		return func(i, j int) bool { return ml[i].Zid < ml[j].Zid }

	} else if keyType == meta.TypeBool {









		if descending {
			return func(i, j int) bool {
				left := ml[i].GetBool(key)
				if left == ml[j].GetBool(key) {
					return i > j
				}
				return left
			}
		}
		return func(i, j int) bool {
			right := ml[j].GetBool(key)
			if ml[i].GetBool(key) == right {
				return i < j
			}
			return right
		}

	} else if keyType == meta.TypeNumber {

		if descending {
			return func(i, j int) bool {
				iVal, iOk := getNum(ml[i], key)
				jVal, jOk := getNum(ml[j], key)
				return (iOk && (!jOk || iVal > jVal)) || !jOk
			}
		}
		return func(i, j int) bool {
			iVal, iOk := getNum(ml[i], key)
			jVal, jOk := getNum(ml[j], key)
			return (iOk && (!jOk || iVal < jVal)) || !jOk
		}
	}


	if descending {
		return func(i, j int) bool {
			iVal, iOk := ml[i].Get(key)
			jVal, jOk := ml[j].Get(key)
			return (iOk && (!jOk || iVal > jVal)) || !jOk
		}
	}







|





|
















|






>
|
>
>
>
>
>
>
>
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>
|
>
|
|
|
|
|
|
|
|
|
|
|
|
|

>







42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
			func(i, j int) bool {
				return metaList[i].Zid > metaList[j].Zid
			})
		return metaList
	}

	if s.Order == "" {
		sort.Slice(metaList, createSortFunc(meta.KeyID, true, metaList))
	} else if s.Order == RandomOrder {
		rand.Shuffle(len(metaList), func(i, j int) {
			metaList[i], metaList[j] = metaList[j], metaList[i]
		})
	} else {
		sort.Slice(metaList, createSortFunc(s.Order, s.Descending, metaList))
	}

	if s.Offset > 0 {
		if s.Offset > len(metaList) {
			return nil
		}
		metaList = metaList[s.Offset:]
	}
	if s.Limit > 0 && s.Limit < len(metaList) {
		metaList = metaList[:s.Limit]
	}
	return metaList
}

type sortFunc func(i, j int) bool

func createSortFunc(key string, descending bool, ml []*meta.Meta) sortFunc {
	keyType := meta.Type(key)
	if key == meta.KeyID || keyType == meta.TypeCredential {
		if descending {
			return func(i, j int) bool { return ml[i].Zid > ml[j].Zid }
		}
		return func(i, j int) bool { return ml[i].Zid < ml[j].Zid }
	}
	if keyType == meta.TypeBool {
		return createSortBoolFunc(ml, key, descending)
	}
	if keyType == meta.TypeNumber {
		return createSortNumberFunc(ml, key, descending)
	}
	return createSortStringFunc(ml, key, descending)
}

func createSortBoolFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			left := ml[i].GetBool(key)
			if left == ml[j].GetBool(key) {
				return i > j
			}
			return left
		}
	}
	return func(i, j int) bool {
		right := ml[j].GetBool(key)
		if ml[i].GetBool(key) == right {
			return i < j
		}
		return right
	}
}

func createSortNumberFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			iVal, iOk := getNum(ml[i], key)
			jVal, jOk := getNum(ml[j], key)
			return (iOk && (!jOk || iVal > jVal)) || !jOk
		}
	}
	return func(i, j int) bool {
		iVal, iOk := getNum(ml[i], key)
		jVal, jOk := getNum(ml[j], key)
		return (iOk && (!jOk || iVal < jVal)) || !jOk
	}
}

func createSortStringFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			iVal, iOk := ml[i].Get(key)
			jVal, jOk := ml[j].Get(key)
			return (iOk && (!jOk || iVal > jVal)) || !jOk
		}
	}

Added strfun/strfun.go.















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// Copyright (c) 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 strfun provides some string functions.
package strfun

import (
	"strings"
	"unicode"
)

// TrimSpaceRight returns a slice of the string s, with all trailing white space removed,
// as defined by Unicode.
func TrimSpaceRight(s string) string {
	return strings.TrimRightFunc(s, unicode.IsSpace)
}

Added strfun/strfun_test.go.





































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 strfun provides some string functions.
package strfun_test

import (
	"testing"

	"zettelstore.de/z/strfun"
)

func TestTrimSpaceRight(t *testing.T) {
	const space = "\t\v\r\f\n\u0085\u00a0\u2000\u3000"
	testcases := []struct {
		in  string
		exp string
	}{
		{"", ""},
		{"abc", "abc"},
		{" ", ""},
		{space, ""},
		{space + "abc" + space, space + "abc"},
		{" \t\r\n \t\t\r\r\n\n ", ""},
		{" \t\r\n x\t\t\r\r\n\n ", " \t\r\n x"},
		{" \u2000\t\r\n x\t\t\r\r\ny\n \u3000", " \u2000\t\r\n x\t\t\r\r\ny"},
		{"1 \t\r\n2", "1 \t\r\n2"},
		{" x\x80", " x\x80"},
		{" x\xc0", " x\xc0"},
		{"x \xc0\xc0 ", "x \xc0\xc0"},
		{"x \xc0", "x \xc0"},
		{"x \xc0 ", "x \xc0"},
		{"x \xc0\xc0 ", "x \xc0\xc0"},
		{"x ☺\xc0\xc0 ", "x ☺\xc0\xc0"},
		{"x ☺ ", "x ☺"},
	}
	for i, tc := range testcases {
		got := strfun.TrimSpaceRight(tc.in)
		if got != tc.exp {
			t.Errorf("%d/%q: expected %q, got %q", i, tc.in, tc.exp, got)
		}
	}
}

Changes to template/mustache.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//
224
225
226
227
228
229
230




231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
	}, nil
}

type tagReadingResult struct {
	tag        string
	standalone bool
}





func (tmpl *Template) readTag(mayStandalone bool) (*tagReadingResult, error) {
	var text string
	var err error
	if tmpl.p < len(tmpl.data) && tmpl.data[tmpl.p] == '{' {
		text, err = tmpl.readString("}" + tmpl.ctag)
	} else {
		text, err = tmpl.readString(tmpl.ctag)
	}

	if err == io.EOF {
		//put the remaining text in a block
		return nil, parseError{tmpl.curline, "unmatched open tag"}
	}

	text = text[:len(text)-len(tmpl.ctag)]

	//trim the close tag off the text
	tag := strings.TrimSpace(text)
	if len(tag) == 0 {
		return nil, parseError{tmpl.curline, "empty tag"}
	}

	eow := tmpl.p
	for i := tmpl.p; i < len(tmpl.data); i++ {
		if !(tmpl.data[i] == ' ' || tmpl.data[i] == '\t') {
			eow = i
			break
		}
	}

	// Skip all whitespaces apeared after these types of tags until end of line if
	// the line only contains a tag and whitespaces.
	const skipWhitespaceTagTypes = "#^/<>=!"

	standalone := true
	if mayStandalone {
		if !strings.Contains(skipWhitespaceTagTypes, tag[0:1]) {
			standalone = false
		} else {
			if eow == len(tmpl.data) {
				standalone = true
				tmpl.p = eow
			} else if eow < len(tmpl.data) && tmpl.data[eow] == '\n' {
				standalone = true







>
>
>
>



















|













<
<


|







224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267


268
269
270
271
272
273
274
275
276
277
	}, nil
}

type tagReadingResult struct {
	tag        string
	standalone bool
}

var skipWhitespaceTagTypes = map[byte]bool{
	'#': true, '^': true, '/': true, '<': true, '>': true, '=': true, '!': true,
}

func (tmpl *Template) readTag(mayStandalone bool) (*tagReadingResult, error) {
	var text string
	var err error
	if tmpl.p < len(tmpl.data) && tmpl.data[tmpl.p] == '{' {
		text, err = tmpl.readString("}" + tmpl.ctag)
	} else {
		text, err = tmpl.readString(tmpl.ctag)
	}

	if err == io.EOF {
		//put the remaining text in a block
		return nil, parseError{tmpl.curline, "unmatched open tag"}
	}

	text = text[:len(text)-len(tmpl.ctag)]

	//trim the close tag off the text
	tag := strings.TrimSpace(text)
	if tag == "" {
		return nil, parseError{tmpl.curline, "empty tag"}
	}

	eow := tmpl.p
	for i := tmpl.p; i < len(tmpl.data); i++ {
		if !(tmpl.data[i] == ' ' || tmpl.data[i] == '\t') {
			eow = i
			break
		}
	}

	// Skip all whitespaces apeared after these types of tags until end of line if
	// the line only contains a tag and whitespaces.


	standalone := true
	if mayStandalone {
		if _, ok := skipWhitespaceTagTypes[tag[0]]; !ok {
			standalone = false
		} else {
			if eow == len(tmpl.data) {
				standalone = true
				tmpl.p = eow
			} else if eow < len(tmpl.data) && tmpl.data[eow] == '\n' {
				standalone = true
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
			section.nodes = append(section.nodes, &textNode{[]byte(padding)})
		}

		tag := tagResult.tag
		switch tag[0] {
		case '!':
			//ignore comment
			break
		case '#', '^':
			name := strings.TrimSpace(tag[1:])
			sn := &sectionNode{name, tag[0] == '^', tmpl.curline, []node{}}
			err := tmpl.parseSection(sn)
			if err != nil {
				return err
			}







<







325
326
327
328
329
330
331

332
333
334
335
336
337
338
			section.nodes = append(section.nodes, &textNode{[]byte(padding)})
		}

		tag := tagResult.tag
		switch tag[0] {
		case '!':
			//ignore comment

		case '#', '^':
			name := strings.TrimSpace(tag[1:])
			sn := &sectionNode{name, tag[0] == '^', tmpl.curline, []node{}}
			err := tmpl.parseSection(sn)
			if err != nil {
				return err
			}
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
			tmpl.nodes = append(tmpl.nodes, &textNode{[]byte(padding)})
		}

		tag := tagResult.tag
		switch tag[0] {
		case '!':
			//ignore comment
			break
		case '#', '^':
			name := strings.TrimSpace(tag[1:])
			sn := &sectionNode{name, tag[0] == '^', tmpl.curline, []node{}}
			err := tmpl.parseSection(sn)
			if err != nil {
				return err
			}







<







400
401
402
403
404
405
406

407
408
409
410
411
412
413
			tmpl.nodes = append(tmpl.nodes, &textNode{[]byte(padding)})
		}

		tag := tagResult.tag
		switch tag[0] {
		case '!':
			//ignore comment

		case '#', '^':
			name := strings.TrimSpace(tag[1:])
			sn := &sectionNode{name, tag[0] == '^', tmpl.curline, []node{}}
			err := tmpl.parseSection(sn)
			if err != nil {
				return err
			}
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
				continue Outer
			default:
				continue Outer
			}
		}
	}
	if errMissing {
		return reflect.Value{}, fmt.Errorf("Missing variable %q", name)
	}
	return reflect.Value{}, nil
}

func isEmpty(v reflect.Value) bool {
	if !v.IsValid() || v.Interface() == nil {
		return true
	}

	valueInd := indirect(v)
	if !valueInd.IsValid() {
		return true
	}
	switch val := valueInd; val.Kind() {
	case reflect.Array, reflect.Slice:
		return val.Len() == 0
	case reflect.String:
		return len(strings.TrimSpace(val.String())) == 0
	default:
		return valueInd.IsZero()
	}
}

func indirect(v reflect.Value) reflect.Value {
loop:







|

















|







495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
				continue Outer
			default:
				continue Outer
			}
		}
	}
	if errMissing {
		return reflect.Value{}, fmt.Errorf("missing variable %q", name)
	}
	return reflect.Value{}, nil
}

func isEmpty(v reflect.Value) bool {
	if !v.IsValid() || v.Interface() == nil {
		return true
	}

	valueInd := indirect(v)
	if !valueInd.IsValid() {
		return true
	}
	switch val := valueInd; val.Kind() {
	case reflect.Array, reflect.Slice:
		return val.Len() == 0
	case reflect.String:
		return strings.TrimSpace(val.String()) == ""
	default:
		return valueInd.IsZero()
	}
}

func indirect(v reflect.Value) reflect.Value {
loop:

Changes to template/mustache_test.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
	}

	// Now set "error on missing varaible" and confirm we get errors.
	for _, test := range missing {
		output, err := renderString(test.tmpl, true, test.context)
		if err == nil {
			t.Errorf("%q expected missing variable error but got %q", test.tmpl, output)
		} else if !strings.Contains(err.Error(), "Missing variable") {
			t.Errorf("%q expected missing variable error but got %q", test.tmpl, err.Error())
		}
	}
}

var malformed = []Test{
	{`{{#a}}{{}}{{/a}}`, Data{true, "hello"}, "", fmt.Errorf("line 1: empty tag")},







|







271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
	}

	// Now set "error on missing varaible" and confirm we get errors.
	for _, test := range missing {
		output, err := renderString(test.tmpl, true, test.context)
		if err == nil {
			t.Errorf("%q expected missing variable error but got %q", test.tmpl, output)
		} else if !strings.Contains(err.Error(), "missing variable") {
			t.Errorf("%q expected missing variable error but got %q", test.tmpl, err.Error())
		}
	}
}

var malformed = []Test{
	{`{{#a}}{{}}{{/a}}`, Data{true, "hello"}, "", fmt.Errorf("line 1: empty tag")},
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
				t.Errorf("expected %d tags, got 0", len(expected[i].Tags))
				return
			}
		case template.Section, template.InvertedSection:
			compareTags(t, tag.Tags(), expected[i].Tags)
		case template.Partial:
			compareTags(t, tag.Tags(), expected[i].Tags)
		case template.Invalid:
			t.Errorf("invalid tag type: %s", tagString(tag.Type()))
			return
		default:
			t.Errorf("invalid tag type: %s", tagString(tag.Type()))
			return
		}
	}
}








<
<
<







463
464
465
466
467
468
469



470
471
472
473
474
475
476
				t.Errorf("expected %d tags, got 0", len(expected[i].Tags))
				return
			}
		case template.Section, template.InvertedSection:
			compareTags(t, tag.Tags(), expected[i].Tags)
		case template.Partial:
			compareTags(t, tag.Tags(), expected[i].Tags)



		default:
			t.Errorf("invalid tag type: %s", tagString(tag.Type()))
			return
		}
	}
}

Changes to template/spec_test.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
	"sort"
	"testing"

	"zettelstore.de/z/template"
)

var enabledTests = map[string]map[string]bool{
	"comments.json": map[string]bool{
		"Inline":                           true,
		"Multiline":                        true,
		"Standalone":                       true,
		"Indented Standalone":              true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
		"Multiline Standalone":             true,
		"Indented Multiline Standalone":    true,
		"Indented Inline":                  true,
		"Surrounding Whitespace":           true,
	},
	"delimiters.json": map[string]bool{
		"Pair Behavior":                    true,
		"Special Characters":               true,
		"Sections":                         true,
		"Inverted Sections":                true,
		"Partial Inheritence":              true,
		"Post-Partial Behavior":            true,
		"Outlying Whitespace (Inline)":     true,
		"Standalone Tag":                   true,
		"Indented Standalone Tag":          true,
		"Pair with Padding":                true,
		"Surrounding Whitespace":           true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
	},
	"interpolation.json": map[string]bool{
		"No Interpolation":                             true,
		"Basic Interpolation":                          true,
		"HTML Escaping":                                true,
		"Triple Mustache":                              true,
		"Ampersand":                                    true,
		"Basic Integer Interpolation":                  true,
		"Triple Mustache Integer Interpolation":        true,







|












|















|







28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
	"sort"
	"testing"

	"zettelstore.de/z/template"
)

var enabledTests = map[string]map[string]bool{
	"comments.json": {
		"Inline":                           true,
		"Multiline":                        true,
		"Standalone":                       true,
		"Indented Standalone":              true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
		"Multiline Standalone":             true,
		"Indented Multiline Standalone":    true,
		"Indented Inline":                  true,
		"Surrounding Whitespace":           true,
	},
	"delimiters.json": {
		"Pair Behavior":                    true,
		"Special Characters":               true,
		"Sections":                         true,
		"Inverted Sections":                true,
		"Partial Inheritence":              true,
		"Post-Partial Behavior":            true,
		"Outlying Whitespace (Inline)":     true,
		"Standalone Tag":                   true,
		"Indented Standalone Tag":          true,
		"Pair with Padding":                true,
		"Surrounding Whitespace":           true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
	},
	"interpolation.json": {
		"No Interpolation":                             true,
		"Basic Interpolation":                          true,
		"HTML Escaping":                                true,
		"Triple Mustache":                              true,
		"Ampersand":                                    true,
		"Basic Integer Interpolation":                  true,
		"Triple Mustache Integer Interpolation":        true,
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
		"Interpolation - Standalone":                   true,
		"Triple Mustache - Standalone":                 true,
		"Ampersand - Standalone":                       true,
		"Interpolation With Padding":                   true,
		"Triple Mustache With Padding":                 true,
		"Ampersand With Padding":                       true,
	},
	"inverted.json": map[string]bool{
		"Falsey":                           true,
		"Truthy":                           true,
		"Context":                          true,
		"List":                             true,
		"Empty List":                       true,
		"Doubled":                          true,
		"Nested (Falsey)":                  true,







|







89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
		"Interpolation - Standalone":                   true,
		"Triple Mustache - Standalone":                 true,
		"Ampersand - Standalone":                       true,
		"Interpolation With Padding":                   true,
		"Triple Mustache With Padding":                 true,
		"Ampersand With Padding":                       true,
	},
	"inverted.json": {
		"Falsey":                           true,
		"Truthy":                           true,
		"Context":                          true,
		"List":                             true,
		"Empty List":                       true,
		"Doubled":                          true,
		"Nested (Falsey)":                  true,
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
		"Padding":                          true,
		"Dotted Names - Broken Chains":     true,
		"Surrounding Whitespace":           true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
	},
	"partials.json": map[string]bool{
		"Basic Behavior":                   true,
		"Failed Lookup":                    true,
		"Context":                          true,
		"Recursion":                        true,
		"Surrounding Whitespace":           true,
		"Inline Indentation":               true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
		"Standalone Indentation":           true,
		"Padding Whitespace":               true,
	},
	"sections.json": map[string]bool{
		"Truthy":                           true,
		"Falsey":                           true,
		"Context":                          true,
		"Deeply Nested Contexts":           true,
		"List":                             true,
		"Empty List":                       true,
		"Doubled":                          true,







|












|







112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
		"Padding":                          true,
		"Dotted Names - Broken Chains":     true,
		"Surrounding Whitespace":           true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
	},
	"partials.json": {
		"Basic Behavior":                   true,
		"Failed Lookup":                    true,
		"Context":                          true,
		"Recursion":                        true,
		"Surrounding Whitespace":           true,
		"Inline Indentation":               true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
		"Standalone Indentation":           true,
		"Padding Whitespace":               true,
	},
	"sections.json": {
		"Truthy":                           true,
		"Falsey":                           true,
		"Context":                          true,
		"Deeply Nested Contexts":           true,
		"List":                             true,
		"Empty List":                       true,
		"Doubled":                          true,

Changes to testdata/content/link/20200215204700.zettel.

1
2
3
4
5
6
7
8



title: Simple Test

[[Home|https://zettelstore.de/z]]
[[https://zettelstore.de]]
[[Config|00000000000100]]
[[00000000000100]]
[[Frag|#frag]]
[[#frag]]











>
>
>
1
2
3
4
5
6
7
8
9
10
11
title: Simple Test

[[Home|https://zettelstore.de/z]]
[[https://zettelstore.de]]
[[Config|00000000000100]]
[[00000000000100]]
[[Frag|#frag]]
[[#frag]]
[[H|/hosted]]
[[B|//based]]
[[R|../rel]]

Changes to tests/markdown_test.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

22
23
24
25
26
27
28
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 tests provides some higher-level tests.
package tests

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"regexp"
	"strings"
	"testing"


	"zettelstore.de/z/encoder"
	_ "zettelstore.de/z/encoder/htmlenc"
	_ "zettelstore.de/z/encoder/jsonenc"
	_ "zettelstore.de/z/encoder/nativeenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/zmkenc"
	"zettelstore.de/z/input"

|



















>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//-----------------------------------------------------------------------------
// 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 tests provides some higher-level tests.
package tests

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"regexp"
	"strings"
	"testing"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	_ "zettelstore.de/z/encoder/htmlenc"
	_ "zettelstore.de/z/encoder/jsonenc"
	_ "zettelstore.de/z/encoder/nativeenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/zmkenc"
	"zettelstore.de/z/input"
61
62
63
64
65
66
67














68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93



94







95
96
97
98
99
100

101




102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121





122
123
124
125
126
127
128
129
130
131
132
133
134

135
136
137
138
139
140
141
142
143
144
145
146
147
148
	"<http://foo.bar.`baz>`\n",                                 // 346
	"[foo<http://example.com/?search=](uri)>\n",                // 522
	"[foo<http://example.com/?search=][ref]>\n\n[ref]: /uri\n", // 534
	"<http://foo.bar.baz/test?q=hello&id=22&boolean>\n",        // 591
}

var reHeadingID = regexp.MustCompile(` id="[^"]*"`)















func TestMarkdownSpec(t *testing.T) {
	content, err := ioutil.ReadFile("../testdata/markdown/spec.json")
	if err != nil {
		panic(err)
	}
	var testcases []markdownTestCase
	if err = json.Unmarshal(content, &testcases); err != nil {
		panic(err)
	}
	for _, format := range formats {
		enc := encoder.Create(format)
		if enc == nil {
			panic(fmt.Sprintf("No encoder for %q found", format))
		}
	}
	excMap := make(map[string]bool, len(exceptions))
	for _, exc := range exceptions {
		excMap[exc] = true
	}
	htmlEncoder := encoder.Create("html", &encoder.BoolOption{Key: "xhtml", Value: true})
	zmkEncoder := encoder.Create("zmk")
	var sb strings.Builder
	for _, tc := range testcases {
		testID := tc.Example*100 + 1
		ast := parser.ParseBlocks(input.NewInput(tc.Markdown), nil, "markdown")











		for _, format := range formats {
			t.Run(fmt.Sprintf("Encode %v %v", format, testID), func(st *testing.T) {
				encoder.Create(format).WriteBlocks(&sb, ast)
				sb.Reset()
			})
		}

		if _, found := excMap[tc.Markdown]; !found {




			t.Run(fmt.Sprintf("Encode md html %v", testID), func(st *testing.T) {
				htmlEncoder.WriteBlocks(&sb, ast)
				gotHTML := sb.String()
				sb.Reset()

				mdHTML := tc.HTML
				mdHTML = strings.ReplaceAll(mdHTML, "\"MAILTO:", "\"mailto:")
				gotHTML = strings.ReplaceAll(gotHTML, " class=\"zs-external\"", "")
				gotHTML = strings.ReplaceAll(gotHTML, "%2A", "*") // url.QueryEscape
				if strings.Count(gotHTML, "<h") > 0 {
					gotHTML = reHeadingID.ReplaceAllString(gotHTML, "")
				}
				if gotHTML != mdHTML {
					mdHTML := strings.ReplaceAll(mdHTML, "<li>\n", "<li>")
					if gotHTML != mdHTML {
						st.Errorf("\nCMD: %q\nExp: %q\nGot: %q", tc.Markdown, mdHTML, gotHTML)
					}
				}
			})
		}





		t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) {
			zmkEncoder.WriteBlocks(&sb, ast)
			gotFirst := sb.String()
			sb.Reset()

			testID = tc.Example*100 + 2
			secondAst := parser.ParseBlocks(input.NewInput(gotFirst), nil, "zmk")
			zmkEncoder.WriteBlocks(&sb, secondAst)
			gotSecond := sb.String()
			sb.Reset()

			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(gotFirst), nil, "zmk")
			zmkEncoder.WriteBlocks(&sb, thirdAst)
			gotThird := sb.String()
			sb.Reset()

			if gotSecond != gotThird {
				st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird)
			}
		})
	}
}







>
>
>
>
>
>
>
>
>
>
>
>
>
>










<
<
<
<
<
<




<
<
<

<

>
>
>
|
>
>
>
>
>
>
>
|
|
|
|
|
|
>
|
>
>
>
>
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>
>
>
>
>
|
|
|
|

|
|
|
|
|

|
|
>
|
<
|
|
|
|
|

|
|
|
|
|

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92






93
94
95
96



97

98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161

162
163
164
165
166
167
168
169
170
171
172
173
	"<http://foo.bar.`baz>`\n",                                 // 346
	"[foo<http://example.com/?search=](uri)>\n",                // 522
	"[foo<http://example.com/?search=][ref]>\n\n[ref]: /uri\n", // 534
	"<http://foo.bar.baz/test?q=hello&id=22&boolean>\n",        // 591
}

var reHeadingID = regexp.MustCompile(` id="[^"]*"`)

func TestEncoderAvailability(t *testing.T) {
	encoderMissing := false
	for _, format := range formats {
		enc := encoder.Create(format)
		if enc == nil {
			t.Errorf("No encoder for %q found", format)
			encoderMissing = true
		}
	}
	if encoderMissing {
		panic("At least one encoder is missing. See test log")
	}
}

func TestMarkdownSpec(t *testing.T) {
	content, err := ioutil.ReadFile("../testdata/markdown/spec.json")
	if err != nil {
		panic(err)
	}
	var testcases []markdownTestCase
	if err = json.Unmarshal(content, &testcases); err != nil {
		panic(err)
	}






	excMap := make(map[string]bool, len(exceptions))
	for _, exc := range exceptions {
		excMap[exc] = true
	}



	for _, tc := range testcases {

		ast := parser.ParseBlocks(input.NewInput(tc.Markdown), nil, "markdown")
		testAllEncodings(t, tc, ast)
		if _, found := excMap[tc.Markdown]; !found {
			testHTMLEncoding(t, tc, ast)
		}
		testZmkEncoding(t, tc, ast)
	}
}

func testAllEncodings(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) {
	var sb strings.Builder
	testID := tc.Example*100 + 1
	for _, format := range formats {
		t.Run(fmt.Sprintf("Encode %v %v", format, testID), func(st *testing.T) {
			encoder.Create(format).WriteBlocks(&sb, ast)
			sb.Reset()
		})
	}
}

func testHTMLEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) {
	htmlEncoder := encoder.Create("html", &encoder.BoolOption{Key: "xhtml", Value: true})
	var sb strings.Builder
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode md html %v", testID), func(st *testing.T) {
		htmlEncoder.WriteBlocks(&sb, ast)
		gotHTML := sb.String()
		sb.Reset()

		mdHTML := tc.HTML
		mdHTML = strings.ReplaceAll(mdHTML, "\"MAILTO:", "\"mailto:")
		gotHTML = strings.ReplaceAll(gotHTML, " class=\"zs-external\"", "")
		gotHTML = strings.ReplaceAll(gotHTML, "%2A", "*") // url.QueryEscape
		if strings.Count(gotHTML, "<h") > 0 {
			gotHTML = reHeadingID.ReplaceAllString(gotHTML, "")
		}
		if gotHTML != mdHTML {
			mdHTML = strings.ReplaceAll(mdHTML, "<li>\n", "<li>")
			if gotHTML != mdHTML {
				st.Errorf("\nCMD: %q\nExp: %q\nGot: %q", tc.Markdown, mdHTML, gotHTML)
			}
		}
	})
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) {
	zmkEncoder := encoder.Create("zmk")
	var sb strings.Builder
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) {
		zmkEncoder.WriteBlocks(&sb, ast)
		gotFirst := sb.String()
		sb.Reset()

		testID = tc.Example*100 + 2
		secondAst := parser.ParseBlocks(input.NewInput(gotFirst), nil, "zmk")
		zmkEncoder.WriteBlocks(&sb, secondAst)
		gotSecond := sb.String()
		sb.Reset()

		// 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(gotFirst), nil, "zmk")
		zmkEncoder.WriteBlocks(&sb, thirdAst)
		gotThird := sb.String()
		sb.Reset()

		if gotSecond != gotThird {
			st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird)
		}
	})

}

Changes to tests/regression_test.go.

83
84
85
86
87
88
89

90
91
92
93
94
95
96
	}
	defer f.Close()
	src, err := ioutil.ReadAll(f)
	return string(src), err
}

func checkFileContent(t *testing.T, filename string, gotContent string) {

	wantContent, err := resultFile(filename)
	if err != nil {
		t.Error(err)
		return
	}
	gotContent = trimLastEOL(gotContent)
	wantContent = trimLastEOL(wantContent)







>







83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
	}
	defer f.Close()
	src, err := ioutil.ReadAll(f)
	return string(src), err
}

func checkFileContent(t *testing.T, filename string, gotContent string) {
	t.Helper()
	wantContent, err := resultFile(filename)
	if err != nil {
		t.Error(err)
		return
	}
	gotContent = trimLastEOL(gotContent)
	wantContent = trimLastEOL(wantContent)
123
124
125
126
127
128
129



































130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179



























180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
	gotSecond := sb.String()
	sb.Reset()

	if gotFirst != gotSecond {
		t.Errorf("\n1st: %q\n2nd: %q", gotFirst, gotSecond)
	}
}




































func TestContentRegression(t *testing.T) {
	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	root, places := getFilePlaces(wd, "content")
	for _, p := range places {
		ss := p.(place.StartStopper)
		if err := ss.Start(context.Background()); err != nil {
			panic(err)
		}
		placeName := p.Location()[len("dir://")+len(root):]
		metaList, err := p.SelectMeta(context.Background(), nil, nil)
		if err != nil {
			panic(err)
		}
		for _, meta := range metaList {
			zettel, err := p.GetZettel(context.Background(), meta.Zid)
			if err != nil {
				panic(err)
			}
			z := parser.ParseZettel(zettel, "")
			for _, format := range formats {
				t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) {
					resultName := filepath.Join(wd, "result", "content", placeName, z.Zid.String()+"."+format)
					checkBlocksFile(st, resultName, z, format)
				})
			}
			t.Run(fmt.Sprintf("%s::%d", p.Location(), meta.Zid), func(st *testing.T) {
				checkZmkEncoder(st, z)
			})
		}
		if err := ss.Stop(context.Background()); err != nil {
			panic(err)
		}
	}
}

func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, format string) {
	t.Helper()

	if enc := encoder.Create(format); enc != nil {
		var sb strings.Builder
		enc.WriteMeta(&sb, zn.Zettel.Meta)
		checkFileContent(t, resultName, sb.String())
		return
	}
	panic(fmt.Sprintf("Unknown writer format %q", format))
}




























func TestMetaRegression(t *testing.T) {
	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	root, places := getFilePlaces(wd, "meta")
	for _, p := range places {
		ss := p.(place.StartStopper)
		if err := ss.Start(context.Background()); err != nil {
			panic(err)
		}
		placeName := p.Location()[len("dir://")+len(root):]
		metaList, err := p.SelectMeta(context.Background(), nil, nil)
		if err != nil {
			panic(err)
		}
		for _, meta := range metaList {
			zettel, err := p.GetZettel(context.Background(), meta.Zid)
			if err != nil {
				panic(err)
			}
			z := parser.ParseZettel(zettel, "")
			for _, format := range formats {
				t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) {
					resultName := filepath.Join(wd, "result", "meta", placeName, z.Zid.String()+"."+format)
					checkMetaFile(st, resultName, z, format)
				})
			}
		}
		if err := ss.Stop(context.Background()); err != nil {
			panic(err)
		}
	}
}







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>








<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>








<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
|
|
<
<
<
<
<
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173




174























175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223


















224

225
226





	gotSecond := sb.String()
	sb.Reset()

	if gotFirst != gotSecond {
		t.Errorf("\n1st: %q\n2nd: %q", gotFirst, gotSecond)
	}
}

func getPlaceName(p place.Place, root string) string {
	return p.Location()[len("dir://")+len(root):]
}

func checkContentPlace(t *testing.T, p place.Place, wd, placeName string) {
	ss := p.(place.StartStopper)
	if err := ss.Start(context.Background()); err != nil {
		panic(err)
	}
	metaList, err := p.SelectMeta(context.Background(), nil, nil)
	if err != nil {
		panic(err)
	}
	for _, meta := range metaList {
		zettel, err := p.GetZettel(context.Background(), meta.Zid)
		if err != nil {
			panic(err)
		}
		z := parser.ParseZettel(zettel, "")
		for _, format := range formats {
			t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) {
				resultName := filepath.Join(wd, "result", "content", placeName, z.Zid.String()+"."+format)
				checkBlocksFile(st, resultName, z, format)
			})
		}
		t.Run(fmt.Sprintf("%s::%d", p.Location(), meta.Zid), func(st *testing.T) {
			checkZmkEncoder(st, z)
		})
	}
	if err := ss.Stop(context.Background()); err != nil {
		panic(err)
	}

}

func TestContentRegression(t *testing.T) {
	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	root, places := getFilePlaces(wd, "content")
	for _, p := range places {




		checkContentPlace(t, p, wd, getPlaceName(p, root))























	}
}

func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, format string) {
	t.Helper()

	if enc := encoder.Create(format); enc != nil {
		var sb strings.Builder
		enc.WriteMeta(&sb, zn.Zettel.Meta)
		checkFileContent(t, resultName, sb.String())
		return
	}
	panic(fmt.Sprintf("Unknown writer format %q", format))
}

func checkMetaPlace(t *testing.T, p place.Place, wd, placeName string) {
	ss := p.(place.StartStopper)
	if err := ss.Start(context.Background()); err != nil {
		panic(err)
	}
	metaList, err := p.SelectMeta(context.Background(), nil, nil)
	if err != nil {
		panic(err)
	}
	for _, meta := range metaList {
		zettel, err := p.GetZettel(context.Background(), meta.Zid)
		if err != nil {
			panic(err)
		}
		z := parser.ParseZettel(zettel, "")
		for _, format := range formats {
			t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) {
				resultName := filepath.Join(wd, "result", "meta", placeName, z.Zid.String()+"."+format)
				checkMetaFile(st, resultName, z, format)
			})
		}
	}
	if err := ss.Stop(context.Background()); err != nil {
		panic(err)
	}
}

func TestMetaRegression(t *testing.T) {
	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	root, places := getFilePlaces(wd, "meta")
	for _, p := range places {


















		checkMetaPlace(t, p, wd, getPlaceName(p, root))

	}
}





Changes to tests/result/content/link/20200215204700.djson.

1
[{"t":"Para","i":[{"t":"Link","q":"external","s":"https://zettelstore.de/z","i":[{"t":"Text","s":"Home"}]},{"t":"Soft"},{"t":"Link","q":"external","s":"https://zettelstore.de","i":[{"t":"Text","s":"https://zettelstore.de"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"Config"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"00000000000100"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"Frag"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"#frag"}]}]}]
|
1
[{"t":"Para","i":[{"t":"Link","q":"external","s":"https://zettelstore.de/z","i":[{"t":"Text","s":"Home"}]},{"t":"Soft"},{"t":"Link","q":"external","s":"https://zettelstore.de","i":[{"t":"Text","s":"https://zettelstore.de"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"Config"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"00000000000100"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"Frag"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"#frag"}]},{"t":"Soft"},{"t":"Link","q":"local","s":"/hosted","i":[{"t":"Text","s":"H"}]},{"t":"Soft"},{"t":"Link","q":"based","s":"/based","i":[{"t":"Text","s":"B"}]},{"t":"Soft"},{"t":"Link","q":"local","s":"../rel","i":[{"t":"Text","s":"R"}]}]}]

Changes to tests/result/content/link/20200215204700.html.

1
2
3
4
5
6



<p><a href="https://zettelstore.de/z" class="zs-external">Home</a>
<a href="https://zettelstore.de" class="zs-external">https://zettelstore.de</a>
<a href="00000000000100">Config</a>
<a href="00000000000100">00000000000100</a>
<a href="#frag">Frag</a>
<a href="#frag">#frag</a></p>








|
>
>
>
1
2
3
4
5
6
7
8
9
<p><a href="https://zettelstore.de/z" class="zs-external">Home</a>
<a href="https://zettelstore.de" class="zs-external">https://zettelstore.de</a>
<a href="00000000000100">Config</a>
<a href="00000000000100">00000000000100</a>
<a href="#frag">Frag</a>
<a href="#frag">#frag</a>
<a href="/hosted">H</a>
<a href="/based">B</a>
<a href="../rel">R</a></p>

Changes to tests/result/content/link/20200215204700.native.

1
[Para Link EXTERNAL "https://zettelstore.de/z" [Text "Home"],Space,Link EXTERNAL "https://zettelstore.de" [],Space,Link ZETTEL "00000000000100" [Text "Config"],Space,Link ZETTEL "00000000000100" [],Space,Link SELF "#frag" [Text "Frag"],Space,Link SELF "#frag" []]
|
1
[Para Link EXTERNAL "https://zettelstore.de/z" [Text "Home"],Space,Link EXTERNAL "https://zettelstore.de" [],Space,Link ZETTEL "00000000000100" [Text "Config"],Space,Link ZETTEL "00000000000100" [],Space,Link SELF "#frag" [Text "Frag"],Space,Link SELF "#frag" [],Space,Link LOCAL "/hosted" [Text "H"],Space,Link BASED "/based" [Text "B"],Space,Link LOCAL "../rel" [Text "R"]]

Changes to tests/result/content/link/20200215204700.text.

1
Home  Config  Frag 
|
1
Home  Config  Frag  H B R

Added tools/build.go.



































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
//-----------------------------------------------------------------------------
// 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 main provides a command to build and run the software.
package main

import (
	"archive/zip"
	"bytes"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

func executeCommand(env []string, name string, arg ...string) (string, error) {
	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)
	}
	if len(env) > 0 {
		env = append(env, os.Environ()...)
	}
	var out bytes.Buffer
	cmd := exec.Command(name, arg...)
	cmd.Env = env
	cmd.Stdin = nil
	cmd.Stdout = &out
	cmd.Stderr = os.Stderr
	err := cmd.Run()
	return out.String(), err
}

func readVersionFile() (string, error) {
	content, err := ioutil.ReadFile("VERSION")
	if err != nil {
		return "", err
	}
	return strings.TrimFunc(string(content), func(r rune) bool {
		return r <= ' '
	}), nil
}

var fossilHash = regexp.MustCompile(`\[[0-9a-fA-F]+\]`)
var dirtyPrefixes = []string{"DELETED", "ADDED", "UPDATED", "CONFLICT", "EDITED", "RENAMED"}

const dirtySuffix = "-dirty"

func readFossilVersion() (string, error) {
	s, err := executeCommand(nil, "fossil", "timeline", "--limit", "1")
	if err != nil {
		return "", err
	}
	hash := fossilHash.FindString(s)
	if len(hash) < 3 {
		return "", errors.New("no fossil hash found")
	}
	hash = hash[1 : len(hash)-1]

	s, err = executeCommand(nil, "fossil", "status")
	if err != nil {
		return "", err
	}
	for _, line := range splitLines(s) {
		for _, prefix := range dirtyPrefixes {
			if strings.HasPrefix(line, prefix) {
				return hash + dirtySuffix, nil
			}
		}
	}
	return hash, nil
}

func splitLines(s string) []string {
	return strings.FieldsFunc(s, func(r rune) bool {
		return r == '\n' || r == '\r'
	})
}

func getVersionData() (string, string) {
	base, err := readVersionFile()
	if err != nil {
		base = "dev"
	}
	fossil, err := readFossilVersion()
	if err != nil {
		return base, ""
	}
	return base, fossil
}

func calcVersion(base, vcs string) string { return base + "+" + vcs }

func getVersion() string {
	base, vcs := getVersionData()
	return calcVersion(base, vcs)
}

func findExec(cmd string) string {
	if path, err := executeCommand(nil, "which", "shadow"); err == nil && path != "" {
		return path
	}
	return ""
}

func cmdCheck() error {
	if err := checkGoTest(); err != nil {
		return err
	}
	if err := checkGoVet(); err != nil {
		return err
	}
	if err := checkGoLint(); err != nil {
		return err
	}
	if err := checkGoVetShadow(); err != nil {
		return err
	}
	return checkFossilExtra()
}

func checkGoTest() error {
	out, err := executeCommand(nil, "go", "test", "./...")
	if err != nil {
		for _, line := range 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 checkGoLint() error {
	out, err := executeCommand(nil, "golint", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some lints failed")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkGoVetShadow() error {
	path := findExec("shadow")
	if path == "" {
		return nil
	}
	out, err := executeCommand(nil, "go", "vet", "-vettool", strings.TrimSpace(path), "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some shadowed variables found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

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 splitLines(out) {
			if i > 0 {
				fmt.Fprint(os.Stderr, ",")
			}
			fmt.Fprintf(os.Stderr, " %q", extra)
		}
		fmt.Fprintln(os.Stderr)
	}
	return nil
}

func cmdBuild() error {
	return doBuild(nil, 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 cmdRelease() error {
	base, fossil := getVersionData()
	if strings.HasSuffix(base, "dev") {
		base = base[:len(base)-3] + "preview-" + time.Now().Format("20060102")
	}
	if strings.HasSuffix(fossil, dirtySuffix) {
		fmt.Fprintf(os.Stderr, "Warning: releasing a dirty version %v\n", fossil)
		base = base + dirtySuffix
	}
	if err := cmdCheck(); err != nil {
		return err
	}
	releases := []struct {
		arch string
		os   string
		env  []string
		name string
	}{
		{"amd64", "linux", nil, "zettelstore"},
		{"arm", "linux", []string{"GOARM=6"}, "zettelstore"},
		{"amd64", "darwin", nil, "iZettelstore"},
		{"arm64", "darwin", nil, "iZettelstore"},
		{"amd64", "windows", nil, "zettelstore.exe"},
	}
	for _, rel := range releases {
		env := append(rel.env, "GOARCH="+rel.arch, "GOOS="+rel.os)
		zsName := filepath.Join("releases", rel.name)
		if err := doBuild(env, calcVersion(base, fossil), zsName); err != nil {
			return err
		}
		zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch)
		if err := createZip(zsName, zipName, rel.name); err != nil {
			return err
		}
		if err := os.Remove(zsName); err != nil {
			return err
		}
	}
	return nil
}

func createZip(zsName, zipName, fileName string) error {
	zsFile, err := os.Open(zsName)
	if err != nil {
		return err
	}
	defer zsFile.Close()
	zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		return err
	}
	defer zipFile.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
	zw := zip.NewWriter(zipFile)
	defer zw.Close()
	w, err := zw.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
		}
	}
	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     Outputs this text.
  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.`)
}

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 "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()
		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)
	}
}

Deleted tools/version.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package main

import (
	"bytes"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"regexp"
	"strings"
)

func readVersionFile() (string, error) {
	content, err := ioutil.ReadFile("VERSION")
	if err != nil {
		return "", err
	}
	return strings.TrimFunc(string(content), func(r rune) bool {
		return r <= ' '
	}), nil
}

var fossilHash = regexp.MustCompile("\\[[0-9a-fA-F]+\\]")
var dirtyPrefixes = []string{"DELETED", "ADDED", "UPDATED", "CONFLICT", "EDITED", "RENAMED"}

func readFossilVersion() (string, error) {
	var out bytes.Buffer
	cmd := exec.Command("fossil", "timeline", "--limit", "1")
	cmd.Stdin = nil
	cmd.Stdout = &out
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return "", err
	}
	hash := fossilHash.FindString(out.String())
	if len(hash) < 3 {
		return "", errors.New("No fossil hash found")
	}
	hash = hash[1 : len(hash)-1]

	out.Reset()
	cmd = exec.Command("fossil", "status")
	cmd.Stdin = nil
	cmd.Stdout = &out
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return "", err
	}
	lines := strings.FieldsFunc(out.String(), func(r rune) bool {
		return r == '\n' || r == '\r'
	})
	for _, line := range lines {
		for _, prefix := range dirtyPrefixes {
			if strings.HasPrefix(line, prefix) {
				return hash + "-dirty", nil
			}
		}
	}
	return hash, nil
}

func main() {
	base, err := readVersionFile()
	if err != nil {
		fmt.Fprintf(os.Stderr, "No VERSION found: %v\n", err)
		base = "dev"
	}
	fossil, err := readFossilVersion()
	if err != nil {
		fmt.Print(base)
	}
	fmt.Printf("%v+%v", base, fossil)
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




















































































































































Changes to usecase/authenticate.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
	return Authenticate{
		port:      port,
		ucGetUser: NewGetUser(port),
	}
}

// Run executes the use case.
func (uc Authenticate) Run(ctx context.Context, ident string, credential string, d time.Duration, k token.Kind) ([]byte, error) {
	identMeta, err := uc.ucGetUser.Run(ctx, ident)
	defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond)

	if identMeta == nil || err != nil {
		compensateCompare()
		return nil, err
	}







|







40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
	return Authenticate{
		port:      port,
		ucGetUser: NewGetUser(port),
	}
}

// Run executes the use case.
func (uc Authenticate) Run(ctx context.Context, ident, credential string, d time.Duration, k token.Kind) ([]byte, error) {
	identMeta, err := uc.ucGetUser.Run(ctx, ident)
	defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond)

	if identMeta == nil || err != nil {
		compensateCompare()
		return nil, err
	}

Added usecase/context.go.



























































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//-----------------------------------------------------------------------------
// 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 provides (business) use cases for the Zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
)

// 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)

	SelectMeta(ctx context.Context, f *place.Filter, s *place.Sorter) ([]*meta.Meta, error)
}

// ZettelContext is the data for this use case.
type ZettelContext struct {
	port ZettelContextPort
}

// NewZettelContext creates a new use case.
func NewZettelContext(port ZettelContextPort) ZettelContext {
	return ZettelContext{port: port}
}

// 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
)

// ParseZCDirection returns a direction value for a given string.
func ParseZCDirection(s string) ZettelContextDirection {
	switch s {
	case "backward":
		return ZettelContextBackward
	case "forward":
		return ZettelContextForward
	}
	return ZettelContextBoth
}

// 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 := ztlCtx{depth: depth}
	uc.addInitialTasks(ctx, &tasks, start)
	visited := id.NewSet()
	isBackward := dir == ZettelContextBoth || dir == ZettelContextBackward
	isForward := dir == ZettelContextBoth || dir == ZettelContextForward
	for !tasks.empty() {
		m, curDepth := tasks.pop()
		if _, ok := visited[m.Zid]; ok {
			continue
		}
		visited[m.Zid] = true
		result = append(result, m)
		if limit > 0 && len(result) > limit { // start is the first element of result
			break
		}
		curDepth++
		for _, p := range m.PairsRest(true) {
			if p.Key == meta.KeyBackward {
				if isBackward {
					uc.addIDSet(ctx, &tasks, curDepth, p.Value)
				}
				continue
			}
			if p.Key == meta.KeyForward {
				if isForward {
					uc.addIDSet(ctx, &tasks, curDepth, p.Value)
				}
				continue
			}
			if p.Key != meta.KeyBack {
				hasInverse := meta.Inverse(p.Key) != ""
				if (!hasInverse || !isBackward) && (hasInverse || !isForward) {
					continue
				}
				if t := meta.Type(p.Key); t == meta.TypeID {
					uc.addID(ctx, &tasks, curDepth, p.Value)
				} else if t == meta.TypeIDSet {
					uc.addIDSet(ctx, &tasks, curDepth, p.Value)
				}
			}
		}
	}
	return result, nil
}

func (uc ZettelContext) addInitialTasks(ctx context.Context, tasks *ztlCtx, start *meta.Meta) {
	tasks.add(start, 0)
	tags, ok := start.GetTags(meta.KeyTags)
	if !ok {
		return
	}
	filter := place.Filter{Expr: map[string][]string{}}
	limit := tasks.depth
	if limit == 0 || limit > 10 {
		limit = 10
	}
	sorter := place.Sorter{Limit: limit}
	for _, tag := range tags {
		filter.Expr[meta.KeyTags] = []string{tag}
		if ml, err := uc.port.SelectMeta(ctx, &filter, &sorter); err == nil {
			for _, m := range ml {
				tasks.add(m, 1)
			}
		}
	}
}

func (uc ZettelContext) addID(ctx context.Context, tasks *ztlCtx, depth int, value string) {
	if zid, err := id.Parse(value); err == nil {
		if m, err := uc.port.GetMeta(ctx, zid); err == nil {
			tasks.add(m, depth)
		}
	}
}

func (uc ZettelContext) addIDSet(ctx context.Context, tasks *ztlCtx, depth int, value string) {
	for _, val := range meta.ListFromValue(value) {
		uc.addID(ctx, tasks, depth, val)
	}
}

type ztlCtxTask struct {
	next  *ztlCtxTask
	meta  *meta.Meta
	depth int
}

type ztlCtx struct {
	first *ztlCtxTask
	last  *ztlCtxTask
	depth int
}

func (zc *ztlCtx) add(m *meta.Meta, depth int) {
	if zc.depth > 0 && depth > zc.depth {
		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 *ztlCtx) empty() bool {
	return zc.first == nil
}

func (zc *ztlCtx) pop() (*meta.Meta, int) {
	task := zc.first
	if task == nil {
		return nil, -1
	}
	zc.first = task.next
	if zc.first == nil {
		zc.last = nil
	}
	return task.meta, task.depth
}

Changes to usecase/copy_zettel.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 provides (business) use cases for the zettelstore.
package usecase

import (
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/meta"

)

// CopyZettel is the data for this use case.
type CopyZettel struct{}

// NewCopyZettel creates a new use case.
func NewCopyZettel() CopyZettel {

|














>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//-----------------------------------------------------------------------------
// Copyright (c) 2020-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 provides (business) use cases for the zettelstore.
package usecase

import (
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/strfun"
)

// CopyZettel is the data for this use case.
type CopyZettel struct{}

// NewCopyZettel creates a new use case.
func NewCopyZettel() CopyZettel {
31
32
33
34
35
36
37

38
39
		if len(title) > 0 {
			title = "Copy of " + title
		} else {
			title = "Copy"
		}
		m.Set(meta.KeyTitle, title)
	}

	return domain.Zettel{Meta: m, Content: origZettel.Content}
}







>
|

32
33
34
35
36
37
38
39
40
41
		if len(title) > 0 {
			title = "Copy of " + title
		} else {
			title = "Copy"
		}
		m.Set(meta.KeyTitle, title)
	}
	content := strfun.TrimSpaceRight(origZettel.Content.AsString())
	return domain.Zettel{Meta: m, Content: domain.Content(content)}
}

Changes to usecase/create_zettel.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/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)
}

|


















>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//-----------------------------------------------------------------------------
// 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 provides (business) use cases for the zettelstore.
package usecase

import (
	"context"

	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/strfun"
)

// 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)
}
50
51
52
53
54
55
56

57
58
		m.Set(meta.KeyRole, runtime.GetDefaultRole())
	}
	if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
		m.Set(meta.KeySyntax, runtime.GetDefaultSyntax())
	}
	m.YamlSep = runtime.GetYAMLHeader()


	return uc.port.CreateZettel(ctx, zettel)
}







>


51
52
53
54
55
56
57
58
59
60
		m.Set(meta.KeyRole, runtime.GetDefaultRole())
	}
	if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
		m.Set(meta.KeySyntax, runtime.GetDefaultSyntax())
	}
	m.YamlSep = runtime.GetYAMLHeader()

	zettel.Content = domain.Content(strfun.TrimSpaceRight(zettel.Content.AsString()))
	return uc.port.CreateZettel(ctx, zettel)
}

Changes to usecase/get_user.go.

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
			return nil, nil
		}
		return identMeta, nil
	}
	// Owner was not found or has another ident. Try via list search.
	filter := place.Filter{
		Expr: map[string][]string{
			meta.KeyRole:   []string{meta.ValueRoleUser},
			meta.KeyUserID: []string{ident},
		},
	}
	metaList, err := uc.port.SelectMeta(ctx, &filter, nil)
	if err != nil {
		return nil, err
	}
	if len(metaList) < 1 {







|
|







57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
			return nil, nil
		}
		return identMeta, nil
	}
	// Owner was not found or has another ident. Try via list search.
	filter := place.Filter{
		Expr: map[string][]string{
			meta.KeyRole:   {meta.ValueRoleUser},
			meta.KeyUserID: {ident},
		},
	}
	metaList, err := uc.port.SelectMeta(ctx, &filter, nil)
	if err != nil {
		return nil, err
	}
	if len(metaList) < 1 {

Changes to usecase/list_tags.go.

45
46
47
48
49
50
51

52
53
54
55
56
57
58
59
60
61
62
63
64
	if err != nil {
		return nil, err
	}
	result := make(TagData)
	for _, m := range metas {
		if tl, ok := m.GetList(meta.KeyTags); ok && len(tl) > 0 {
			for _, t := range tl {

				result[t] = append(result[t], m)
			}
		}
	}
	if minCount > 1 {
		for t, ms := range result {
			if len(ms) < minCount {
				delete(result, t)
			}
		}
	}
	return result, nil
}







>













45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
	if err != nil {
		return nil, err
	}
	result := make(TagData)
	for _, m := range metas {
		if tl, ok := m.GetList(meta.KeyTags); ok && len(tl) > 0 {
			for _, t := range tl {
				t = meta.CleanTag(t)
				result[t] = append(result[t], m)
			}
		}
	}
	if minCount > 1 {
		for t, ms := range result {
			if len(ms) < minCount {
				delete(result, t)
			}
		}
	}
	return result, nil
}

Changes to usecase/new_zettel.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

39
40
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 provides (business) use cases for the zettelstore.
package usecase

import (
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/meta"
)

// NewZettel is the data for this use case.
type NewZettel struct{}

// NewNewZettel creates a new use case.
func NewNewZettel() NewZettel {
	return NewZettel{}
}

// Run executes the use case.
func (uc NewZettel) Run(origZettel domain.Zettel) domain.Zettel {
	m := origZettel.Meta.Clone()
	if role, ok := m.Get(meta.KeyRole); ok && role == meta.ValueRoleNewTemplate {
		const prefix = "new-"
		for _, pair := range m.PairsRest(false) {
			if key := pair.Key; len(key) > len(prefix) && key[0:len(prefix)] == prefix {
				m.Set(key[len(prefix):], pair.Value)
				m.Delete(key)
			}
		}
	}

	return domain.Zettel{Meta: m, Content: origZettel.Content}
}

|













|













<
|
|
|
|
|
|
|
<
>
|

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

30
31
32
33
34
35
36

37
38
39
//-----------------------------------------------------------------------------
// Copyright (c) 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 provides (business) use cases for the zettelstore.
package usecase

import (
	"zettelstore.de/z/domain"
	"zettelstore.de/z/strfun"
)

// NewZettel is the data for this use case.
type NewZettel struct{}

// NewNewZettel creates a new use case.
func NewNewZettel() NewZettel {
	return NewZettel{}
}

// Run executes the use case.
func (uc NewZettel) Run(origZettel domain.Zettel) domain.Zettel {
	m := origZettel.Meta.Clone()

	const prefix = "new-"
	for _, pair := range m.PairsRest(false) {
		if key := pair.Key; len(key) > len(prefix) && key[0:len(prefix)] == prefix {
			m.Set(key[len(prefix):], pair.Value)
			m.Delete(key)
		}
	}

	content := strfun.TrimSpaceRight(origZettel.Content.AsString())
	return domain.Zettel{Meta: m, Content: domain.Content(content)}
}

Added usecase/order.go.















































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//-----------------------------------------------------------------------------
// 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 provides (business) use cases for the Zettelstore.
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
	parseZettel ParseZettel
}

// NewZettelOrder creates a new use case.
func NewZettelOrder(port ZettelOrderPort, parseZettel ParseZettel) ZettelOrder {
	return ZettelOrder{port: port, parseZettel: parseZettel}
}

// 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.parseZettel.Run(ctx, zid, syntax)
	if err != nil {
		return nil, nil, err
	}
	for _, ref := range collect.Order(zn) {
		if zid, err := id.Parse(ref.URL.Path); err == nil {
			if m, err := uc.port.GetMeta(ctx, zid); err == nil {
				result = append(result, m)
			}
		}
	}
	return zn.Zettel.Meta, result, nil
}

Deleted usecase/reload.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 provides (business) use cases for the zettelstore.
package usecase

import (
	"context"
)

// ReloadPort is the interface used by this use case.
type ReloadPort interface {
	// Reload clears all caches, reloads all internal data to reflect changes
	// that were possibly undetected.
	Reload(ctx context.Context) error
}

// Reload is the data for this use case.
type Reload struct {
	port ReloadPort
}

// NewReload creates a new use case.
func NewReload(port ReloadPort) Reload {
	return Reload{port: port}
}

// Run executes the use case.
func (uc Reload) Run(ctx context.Context) error {
	return uc.port.Reload(ctx)
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































Changes to usecase/rename_zettel.go.

46
47
48
49
50
51
52




53
54
55
56
57
58
}

// Run executes the use case.
func (uc RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error {
	noEnrichCtx := index.NoEnrichContext(ctx)
	if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil {
		return err




	}
	if _, err := uc.port.GetMeta(noEnrichCtx, newZid); err == nil {
		return &ErrZidInUse{Zid: newZid}
	}
	return uc.port.RenameZettel(ctx, curZid, newZid)
}







>
>
>
>






46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
}

// Run executes the use case.
func (uc RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error {
	noEnrichCtx := index.NoEnrichContext(ctx)
	if _, err := uc.port.GetMeta(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}
	}
	return uc.port.RenameZettel(ctx, curZid, newZid)
}

Changes to usecase/update_zettel.go.

14
15
16
17
18
19
20

21
22
23
24
25
26
27
import (
	"context"

	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/index"

)

// 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)








>







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import (
	"context"

	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/index"
	"zettelstore.de/z/strfun"
)

// 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)

51
52
53
54
55
56
57
58
59
60
61
	}
	m.SetNow(meta.KeyModified)
	m.YamlSep = oldZettel.Meta.YamlSep
	if m.Zid == id.ConfigurationZid {
		m.Set(meta.KeySyntax, meta.ValueSyntaxNone)
	}
	if !hasContent {
		zettel.Content = oldZettel.Content
	}
	return uc.port.UpdateZettel(ctx, zettel)
}







|



52
53
54
55
56
57
58
59
60
61
62
	}
	m.SetNow(meta.KeyModified)
	m.YamlSep = oldZettel.Meta.YamlSep
	if m.Zid == id.ConfigurationZid {
		m.Set(meta.KeySyntax, meta.ValueSyntaxNone)
	}
	if !hasContent {
		zettel.Content = domain.Content(strfun.TrimSpaceRight(oldZettel.Content.AsString()))
	}
	return uc.port.UpdateZettel(ctx, zettel)
}

Changes to web/adapter/api/get_links.go.

95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
				outData.Images.External = stringRefs(extRefs)
			}
		}
		if kind&kindCite != 0 {
			outData.Cites = stringCites(summary.Cites)
		}

		w.Header().Set("Content-Type", format2ContentType("json"))
		enc := json.NewEncoder(w)
		enc.SetEscapeHTML(false)
		err = enc.Encode(&outData)
	}
}

func idURLRefs(refs []*ast.Reference) []jsonIDURL {
	result := make([]jsonIDURL, 0, len(refs))
	for _, ref := range refs {
		path := ref.URL.Path







|


|







95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
				outData.Images.External = stringRefs(extRefs)
			}
		}
		if kind&kindCite != 0 {
			outData.Cites = stringCites(summary.Cites)
		}

		w.Header().Set(adapter.ContentType, format2ContentType("json"))
		enc := json.NewEncoder(w)
		enc.SetEscapeHTML(false)
		enc.Encode(&outData)
	}
}

func idURLRefs(refs []*ast.Reference) []jsonIDURL {
	result := make([]jsonIDURL, 0, len(refs))
	for _, ref := range refs {
		path := ref.URL.Path
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
}

func validKindMatter(kind kindType, matter matterType) bool {
	if kind == 0 {
		return false
	}
	if kind&kindLink != 0 {
		if matter == 0 {
			return false
		}
		return true
	}
	if kind&kindImage != 0 {
		if matter == 0 || matter == matterIncoming {
			return false
		}
		return true
	}
	if kind&kindCite != 0 {
		return matter == matterOutgoing
	}
	return false
}







<
<
<
|












199
200
201
202
203
204
205



206
207
208
209
210
211
212
213
214
215
216
217
218
}

func validKindMatter(kind kindType, matter matterType) bool {
	if kind == 0 {
		return false
	}
	if kind&kindLink != 0 {



		return matter != 0
	}
	if kind&kindImage != 0 {
		if matter == 0 || matter == matterIncoming {
			return false
		}
		return true
	}
	if kind&kindCite != 0 {
		return matter == matterOutgoing
	}
	return false
}

Added web/adapter/api/get_order.go.















































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//-----------------------------------------------------------------------------
// 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 provides api handlers for web requests.
package api

import (
	"net/http"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetOrderHandler creates a new API handler to return zettel references
// of a given zettel.
func 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
		}
		q := r.URL.Query()
		start, metas, err := zettelOrder.Run(r.Context(), zid, q.Get("syntax"))
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		writeMetaList(w, start, metas)
	}
}

Changes to web/adapter/api/get_role_list.go.

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
			adapter.ReportUsecaseError(w, err)
			return
		}

		format := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat())
		switch format {
		case "json":
			w.Header().Set("Content-Type", format2ContentType(format))
			renderListRoleJSON(w, roleList)
		default:
			adapter.BadRequest(w, fmt.Sprintf("Role list not available in format %q", format))
		}

	}
}







|







29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
			adapter.ReportUsecaseError(w, err)
			return
		}

		format := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat())
		switch format {
		case "json":
			w.Header().Set(adapter.ContentType, format2ContentType(format))
			renderListRoleJSON(w, roleList)
		default:
			adapter.BadRequest(w, fmt.Sprintf("Role list not available in format %q", format))
		}

	}
}

Changes to web/adapter/api/get_tags_list.go.

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
			adapter.ReportUsecaseError(w, err)
			return
		}

		format := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat())
		switch format {
		case "json":
			w.Header().Set("Content-Type", format2ContentType(format))
			renderListTagsJSON(w, tagData)
		default:
			adapter.BadRequest(w, fmt.Sprintf("Tags list not available in format %q", format))
		}
	}
}








|







32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
			adapter.ReportUsecaseError(w, err)
			return
		}

		format := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat())
		switch format {
		case "json":
			w.Header().Set(adapter.ContentType, format2ContentType(format))
			renderListTagsJSON(w, tagData)
		default:
			adapter.BadRequest(w, fmt.Sprintf("Tags list not available in format %q", format))
		}
	}
}

Changes to web/adapter/api/get_zettel.go.

15
16
17
18
19
20
21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37




38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
	"fmt"
	"net/http"

	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"

	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetZettelHandler creates a new HTTP handler to return a rendered zettel.
func MakeGetZettelHandler(
	parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) 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()




		zn, err := parseZettel.Run(ctx, zid, q.Get("syntax"))
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}

		format := adapter.GetFormat(r, q, encoder.GetDefaultFormat())
		part := getPart(q, partZettel)
		switch format {
		case "json", "djson":
			if part == partUnknown {
				adapter.BadRequest(w, "Unknown _part parameter")
				return
			}
			w.Header().Set("Content-Type", format2ContentType(format))
			if format != "djson" {
				err = writeJSONZettel(w, zn, part)
			} else {
				err = writeDJSONZettel(ctx, w, zn, part, partZettel, getMeta)
			}
			if err != nil {
				adapter.InternalServerError(w, "Write D/JSON", err)







>
















>
>
>
>






<







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
	"fmt"
	"net/http"

	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/index"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetZettelHandler creates a new HTTP handler to return a rendered zettel.
func MakeGetZettelHandler(
	parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) 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()
		format := adapter.GetFormat(r, q, encoder.GetDefaultFormat())
		if format == "raw" {
			ctx = index.NoEnrichContext(ctx)
		}
		zn, err := parseZettel.Run(ctx, zid, q.Get("syntax"))
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}


		part := getPart(q, partZettel)
		switch format {
		case "json", "djson":
			if part == partUnknown {
				adapter.BadRequest(w, "Unknown _part parameter")
				return
			}
			w.Header().Set(adapter.ContentType, format2ContentType(format))
			if format != "djson" {
				err = writeJSONZettel(w, zn, part)
			} else {
				err = writeDJSONZettel(ctx, w, zn, part, partZettel, getMeta)
			}
			if err != nil {
				adapter.InternalServerError(w, "Write D/JSON", err)
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
		}
		imageAdapter := encoder.AdaptImageOption{Adapter: adapter.MakeImageAdapter()}

		switch part {
		case partZettel:
			inhMeta := false
			if format != "raw" {
				w.Header().Set("Content-Type", format2ContentType(format))
				inhMeta = true
			}
			enc := encoder.Create(format, &langOption,
				&linkAdapter,
				&imageAdapter,
				&encoder.StringsOption{
					Key: "no-meta",
					Value: []string{
						meta.KeyLang,
					},
				},
			)
			if enc == nil {
				err = adapter.ErrNoSuchFormat
			} else {
				_, err = enc.WriteZettel(w, zn, inhMeta)
			}
		case partMeta:
			w.Header().Set("Content-Type", format2ContentType(format))
			if format == "raw" {
				// Don't write inherited meta data, just the raw
				err = writeMeta(w, zn.Zettel.Meta, format)
			} else {
				err = writeMeta(w, zn.InhMeta, format)
			}
		case partContent:
			if format == "raw" {
				if ct, ok := syntax2contentType(runtime.GetSyntax(zn.Zettel.Meta)); ok {
					w.Header().Add("Content-Type", ct)
				}
			} else {
				w.Header().Set("Content-Type", format2ContentType(format))
			}
			err = writeContent(w, zn, format,
				&langOption,
				&encoder.StringOption{
					Key:   meta.KeyMarkerExternal,
					Value: runtime.GetMarkerExternal()},
				&linkAdapter,







|


















|









|


|







71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
		}
		imageAdapter := encoder.AdaptImageOption{Adapter: adapter.MakeImageAdapter()}

		switch part {
		case partZettel:
			inhMeta := false
			if format != "raw" {
				w.Header().Set(adapter.ContentType, format2ContentType(format))
				inhMeta = true
			}
			enc := encoder.Create(format, &langOption,
				&linkAdapter,
				&imageAdapter,
				&encoder.StringsOption{
					Key: "no-meta",
					Value: []string{
						meta.KeyLang,
					},
				},
			)
			if enc == nil {
				err = adapter.ErrNoSuchFormat
			} else {
				_, err = enc.WriteZettel(w, zn, inhMeta)
			}
		case partMeta:
			w.Header().Set(adapter.ContentType, format2ContentType(format))
			if format == "raw" {
				// Don't write inherited meta data, just the raw
				err = writeMeta(w, zn.Zettel.Meta, format)
			} else {
				err = writeMeta(w, zn.InhMeta, format)
			}
		case partContent:
			if format == "raw" {
				if ct, ok := syntax2contentType(runtime.GetSyntax(zn.Zettel.Meta)); ok {
					w.Header().Add(adapter.ContentType, ct)
				}
			} else {
				w.Header().Set(adapter.ContentType, format2ContentType(format))
			}
			err = writeContent(w, zn, format,
				&langOption,
				&encoder.StringOption{
					Key:   meta.KeyMarkerExternal,
					Value: runtime.GetMarkerExternal()},
				&linkAdapter,

Added web/adapter/api/get_zettel_context.go.

































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//-----------------------------------------------------------------------------
// 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 api provides api handlers for web requests.
package api

import (
	"net/http"

	"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 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 := usecase.ParseZCDirection(q.Get("dir"))
		depth, ok := adapter.GetInteger(q, "depth")
		if !ok || depth < 0 {
			depth = 5
		}
		limit, ok := adapter.GetInteger(q, "limit")
		if !ok || limit < 0 {
			limit = 200
		}
		ctx := r.Context()
		metaList, err := getContext.Run(ctx, zid, dir, depth, limit)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		writeMetaList(w, metaList[0], metaList[1:])
	}
}

Changes to web/adapter/api/get_zettel_list.go.

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
		}
		metaList, err := listMeta.Run(ctx1, filter, sorter)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}

		w.Header().Set("Content-Type", format2ContentType(format))
		switch format {
		case "html":
			renderListMetaHTML(w, metaList)
		case "json", "djson":
			renderListMetaXJSON(ctx, w, metaList, format, part, partMeta, getMeta, parseZettel)
		case "native", "raw", "text", "zmk":
			adapter.NotImplemented(w, fmt.Sprintf("Zettel list in format %q not yet implemented", format))







|







42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
		}
		metaList, err := listMeta.Run(ctx1, filter, sorter)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}

		w.Header().Set(adapter.ContentType, format2ContentType(format))
		switch format {
		case "html":
			renderListMetaHTML(w, metaList)
		case "json", "djson":
			renderListMetaXJSON(ctx, w, metaList, format, part, partMeta, getMeta, parseZettel)
		case "native", "raw", "text", "zmk":
			adapter.NotImplemented(w, fmt.Sprintf("Zettel list in format %q not yet implemented", format))

Changes to web/adapter/api/json.go.

40
41
42
43
44
45
46






47
48
49
50
51
52
53
	Content  interface{}       `json:"content"`
}
type jsonMeta struct {
	ID   string            `json:"id"`
	URL  string            `json:"url"`
	Meta map[string]string `json:"meta"`
}






type jsonContent struct {
	ID       string      `json:"id"`
	URL      string      `json:"url"`
	Encoding string      `json:"encoding"`
	Content  interface{} `json:"content"`
}








>
>
>
>
>
>







40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
	Content  interface{}       `json:"content"`
}
type jsonMeta struct {
	ID   string            `json:"id"`
	URL  string            `json:"url"`
	Meta map[string]string `json:"meta"`
}
type jsonMetaList struct {
	ID   string            `json:"id"`
	URL  string            `json:"url"`
	Meta map[string]string `json:"meta"`
	List []jsonMeta        `json:"list"`
}
type jsonContent struct {
	ID       string      `json:"id"`
	URL      string      `json:"url"`
	Encoding string      `json:"encoding"`
	Content  interface{} `json:"content"`
}

83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
			Content:  content,
		}
	case partID:
		outData = idData
	default:
		panic(part)
	}
	enc := json.NewEncoder(w)
	enc.SetEscapeHTML(false)
	return enc.Encode(outData)
}

func encodedContent(content domain.Content) (string, interface{}) {
	if content.IsBinary() {
		return "base64", content.AsBytes()
	}
	return "", content.AsString()







<
<
|







89
90
91
92
93
94
95


96
97
98
99
100
101
102
103
			Content:  content,
		}
	case partID:
		outData = idData
	default:
		panic(part)
	}


	return encodeJSONData(w, outData, false)
}

func encodedContent(content domain.Content) (string, interface{}) {
	if content.IsBinary() {
		return "base64", content.AsBytes()
	}
	return "", content.AsString()
284
285
286
287
288
289
290






















	if enc == nil {
		return adapter.ErrNoSuchFormat
	}

	_, err := enc.WriteMeta(w, m)
	return err
}





























>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
	if enc == nil {
		return adapter.ErrNoSuchFormat
	}

	_, err := enc.WriteMeta(w, m)
	return err
}

func encodeJSONData(w http.ResponseWriter, data interface{}, addHeader bool) error {
	w.Header().Set(adapter.ContentType, format2ContentType("json"))
	enc := json.NewEncoder(w)
	enc.SetEscapeHTML(false)
	return enc.Encode(data)
}

func writeMetaList(w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error {
	outData := jsonMetaList{
		ID:   m.Zid.String(),
		URL:  adapter.NewURLBuilder('z').SetZid(m.Zid).String(),
		Meta: m.Map(),
		List: make([]jsonMeta, len(metaList)),
	}
	for i, m := range metaList {
		outData.List[i].ID = m.Zid.String()
		outData.List[i].URL = adapter.NewURLBuilder('z').SetZid(m.Zid).String()
		outData.List[i].Meta = m.Map()
	}
	return encodeJSONData(w, outData, true)
}

Changes to web/adapter/api/login.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	"zettelstore.de/z/web/session"
)

// MakePostLoginHandlerAPI creates a new HTTP handler to authenticate the given user via API.
func MakePostLoginHandlerAPI(auth usecase.Authenticate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !startup.WithAuth() {
			w.Header().Set("Content-Type", format2ContentType("json"))
			writeJSONToken(w, "freeaccess", 24*366*10*time.Hour)
			return
		}
		_, apiDur := startup.TokenLifetime()
		authenticateViaJSON(auth, w, r, apiDur)
	}
}







|







23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	"zettelstore.de/z/web/session"
)

// MakePostLoginHandlerAPI creates a new HTTP handler to authenticate the given user via API.
func MakePostLoginHandlerAPI(auth usecase.Authenticate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !startup.WithAuth() {
			w.Header().Set(adapter.ContentType, format2ContentType("json"))
			writeJSONToken(w, "freeaccess", 24*366*10*time.Hour)
			return
		}
		_, apiDur := startup.TokenLifetime()
		authenticateViaJSON(auth, w, r, apiDur)
	}
}
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
	}
	if token == nil {
		w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`)
		http.Error(w, "Authentication failed", http.StatusUnauthorized)
		return
	}

	w.Header().Set("Content-Type", format2ContentType("json"))
	writeJSONToken(w, string(token), authDuration)
}

func authenticateForJSON(
	auth usecase.Authenticate,
	w http.ResponseWriter,
	r *http.Request,







|







49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
	}
	if token == nil {
		w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`)
		http.Error(w, "Authentication failed", http.StatusUnauthorized)
		return
	}

	w.Header().Set(adapter.ContentType, format2ContentType("json"))
	writeJSONToken(w, string(token), authDuration)
}

func authenticateForJSON(
	auth usecase.Authenticate,
	w http.ResponseWriter,
	r *http.Request,
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
			adapter.BadRequest(w, "Not authenticated")
			return
		}
		totalLifetime := auth.Expires.Sub(auth.Issued)
		currentLifetime := auth.Now.Sub(auth.Issued)
		// If we are in the first quarter of the tokens lifetime, return the token
		if currentLifetime*4 < totalLifetime {
			w.Header().Set("Content-Type", format2ContentType("json"))
			writeJSONToken(w, string(auth.Token), totalLifetime-currentLifetime)
			return
		}

		// Toke is a little bit aged. Create a new one
		_, apiDur := startup.TokenLifetime()
		token, err := token.GetToken(auth.User, apiDur, token.KindJSON)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		w.Header().Set("Content-Type", format2ContentType("json"))
		writeJSONToken(w, string(token), apiDur)
	}
}







|











|



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
			adapter.BadRequest(w, "Not authenticated")
			return
		}
		totalLifetime := auth.Expires.Sub(auth.Issued)
		currentLifetime := auth.Now.Sub(auth.Issued)
		// If we are in the first quarter of the tokens lifetime, return the token
		if currentLifetime*4 < totalLifetime {
			w.Header().Set(adapter.ContentType, format2ContentType("json"))
			writeJSONToken(w, string(auth.Token), totalLifetime-currentLifetime)
			return
		}

		// Toke is a little bit aged. Create a new one
		_, apiDur := startup.TokenLifetime()
		token, err := token.GetToken(auth.User, apiDur, token.KindJSON)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		w.Header().Set(adapter.ContentType, format2ContentType("json"))
		writeJSONToken(w, string(token), apiDur)
	}
}

Deleted web/adapter/api/reload.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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"
)

// ReloadHandlerAPI creates a new HTTP handler for the use case "reload".
func ReloadHandlerAPI(w http.ResponseWriter, r *http.Request, format string) {
	w.Header().Set("Content-Type", format2ContentType(format))
	w.WriteHeader(http.StatusNoContent)
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































Changes to web/adapter/api/request.go.

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
	"meta":    partMeta,
	"content": partContent,
	"zettel":  partZettel,
}

func getPart(q url.Values, defPart partType) partType {
	p := q.Get("_part")
	if len(p) == 0 {
		return defPart
	}
	if part, ok := partMap[p]; ok {
		return part
	}
	return partUnknown
}







|







28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
	"meta":    partMeta,
	"content": partContent,
	"zettel":  partZettel,
}

func getPart(q url.Values, defPart partType) partType {
	p := q.Get("_part")
	if p == "" {
		return defPart
	}
	if part, ok := partMap[p]; ok {
		return part
	}
	return partUnknown
}

Changes to web/adapter/encoding.go.

13
14
15
16
17
18
19

20
21
22
23
24
25
26

import (
	"context"
	"errors"
	"strings"

	"zettelstore.de/z/ast"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/index"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
)








>







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

import (
	"context"
	"errors"
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/config/startup"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/index"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
)

47
48
49
50
51
52
53










54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
	ctx context.Context,
	key byte,
	getMeta usecase.GetMeta,
	part, format string,
) func(*ast.LinkNode) ast.InlineNode {
	return func(origLink *ast.LinkNode) ast.InlineNode {
		origRef := origLink.Ref










		if origRef == nil || origRef.State != ast.RefStateZettel {
			return origLink
		}
		zid, err := id.Parse(origRef.URL.Path)
		if err != nil {
			panic(err)
		}
		_, err = getMeta.Run(index.NoEnrichContext(ctx), zid)
		newLink := *origLink
		if err == nil {
			u := NewURLBuilder(key).SetZid(zid)
			if part != "" {
				u.AppendQuery("_part", part)
			}
			if format != "" {
				u.AppendQuery("_format", format)
			}
			if fragment := origRef.URL.EscapedFragment(); len(fragment) > 0 {
				u.SetFragment(fragment)
			}
			newRef := ast.ParseReference(u.String())
			newRef.State = ast.RefStateZettelFound
			newLink.Ref = newRef
			return &newLink
		}
		if place.IsErrNotAllowed(err) {
			return &ast.FormatNode{
				Code:    ast.FormatSpan,
				Attrs:   origLink.Attrs,
				Inlines: origLink.Inlines,
			}
		}
		newRef := ast.ParseReference(origRef.Value)
		newRef.State = ast.RefStateZettelBroken
		newLink.Ref = newRef
		return &newLink
	}
}

// MakeImageAdapter creates an adapter to change an image node during encoding.
func MakeImageAdapter() func(*ast.ImageNode) ast.InlineNode {
	return func(origImage *ast.ImageNode) ast.InlineNode {
		if origImage.Ref == nil || origImage.Ref.State != ast.RefStateZettel {
			return origImage
		}
		newImage := *origImage
		zid, err := id.Parse(newImage.Ref.Value)
		if err != nil {
			panic(err)
		}
		newImage.Ref = ast.ParseReference(
			NewURLBuilder('z').SetZid(zid).AppendQuery("_part", "content").AppendQuery(
				"_format", "raw").String())
		newImage.Ref.State = ast.RefStateZettelFound
		return &newImage
	}
}







>
>
>
>
>
>
>
>
>
>
|




















|











|



















|



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
	ctx context.Context,
	key byte,
	getMeta usecase.GetMeta,
	part, format string,
) func(*ast.LinkNode) ast.InlineNode {
	return func(origLink *ast.LinkNode) ast.InlineNode {
		origRef := origLink.Ref
		if origRef == nil {
			return origLink
		}
		if origRef.State == ast.RefStateBased {
			newLink := *origLink
			newRef := ast.ParseReference(startup.URLPrefix() + origRef.Value[1:])
			newRef.State = ast.RefStateHosted
			newLink.Ref = newRef
			return &newLink
		}
		if origRef.State != ast.RefStateZettel {
			return origLink
		}
		zid, err := id.Parse(origRef.URL.Path)
		if err != nil {
			panic(err)
		}
		_, err = getMeta.Run(index.NoEnrichContext(ctx), zid)
		newLink := *origLink
		if err == nil {
			u := NewURLBuilder(key).SetZid(zid)
			if part != "" {
				u.AppendQuery("_part", part)
			}
			if format != "" {
				u.AppendQuery("_format", format)
			}
			if fragment := origRef.URL.EscapedFragment(); len(fragment) > 0 {
				u.SetFragment(fragment)
			}
			newRef := ast.ParseReference(u.String())
			newRef.State = ast.RefStateFound
			newLink.Ref = newRef
			return &newLink
		}
		if place.IsErrNotAllowed(err) {
			return &ast.FormatNode{
				Code:    ast.FormatSpan,
				Attrs:   origLink.Attrs,
				Inlines: origLink.Inlines,
			}
		}
		newRef := ast.ParseReference(origRef.Value)
		newRef.State = ast.RefStateBroken
		newLink.Ref = newRef
		return &newLink
	}
}

// MakeImageAdapter creates an adapter to change an image node during encoding.
func MakeImageAdapter() func(*ast.ImageNode) ast.InlineNode {
	return func(origImage *ast.ImageNode) ast.InlineNode {
		if origImage.Ref == nil || origImage.Ref.State != ast.RefStateZettel {
			return origImage
		}
		newImage := *origImage
		zid, err := id.Parse(newImage.Ref.Value)
		if err != nil {
			panic(err)
		}
		newImage.Ref = ast.ParseReference(
			NewURLBuilder('z').SetZid(zid).AppendQuery("_part", "content").AppendQuery(
				"_format", "raw").String())
		newImage.Ref.State = ast.RefStateFound
		return &newImage
	}
}

Deleted web/adapter/reload.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 adapter provides handlers for web requests.
package adapter

import (
	"net/http"

	"zettelstore.de/z/encoder"
	"zettelstore.de/z/usecase"
)

// MakeReloadHandler creates a new HTTP handler for the use case "reload".
func MakeReloadHandler(
	reload usecase.Reload,
	apiHandler func(http.ResponseWriter, *http.Request, string),
	htmlHandler http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		err := reload.Run(r.Context())
		if err != nil {
			ReportUsecaseError(w, err)
			return
		}

		if format := GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()); format != "html" {
			apiHandler(w, r, format)
		}
		htmlHandler(w, r)
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































Changes to web/adapter/request.go.

16
17
18
19
20
21
22














23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
	"net/url"
	"strconv"
	"strings"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
)















// GetFormat returns the data format selected by the caller.
func GetFormat(r *http.Request, q url.Values, defFormat string) string {
	format := q.Get("_format")
	if len(format) > 0 {
		return format
	}
	if format, ok := getOneFormat(r, "Accept"); ok {
		return format
	}
	if format, ok := getOneFormat(r, "Content-Type"); ok {
		return format
	}
	return defFormat
}

func getOneFormat(r *http.Request, key string) (string, bool) {
	if values, ok := r.Header[key]; ok {







>
>
>
>
>
>
>
>
>
>
>
>
>
>










|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
	"net/url"
	"strconv"
	"strings"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
)

// 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
}

// ContentType defines the HTTP header value "Content-Type".
const ContentType = "Content-Type"

// GetFormat returns the data format selected by the caller.
func GetFormat(r *http.Request, q url.Values, defFormat string) string {
	format := q.Get("_format")
	if len(format) > 0 {
		return format
	}
	if format, ok := getOneFormat(r, "Accept"); ok {
		return format
	}
	if format, ok := getOneFormat(r, ContentType); ok {
		return format
	}
	return defFormat
}

func getOneFormat(r *http.Request, key string) (string, bool) {
	if values, ok := r.Header[key]; ok {
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
					sorter.Limit = limit
				}
			}
		case negateQKey:
			filter = place.EnsureFilter(filter)
			filter.Negate = true
		case sQKey:
			if values := cleanQueryValues(values); len(values) > 0 {
				filter = place.EnsureFilter(filter)
				filter.Expr[""] = values
			}
		default:
			if !forSearch && meta.KeyIsValid(key) {
				filter = place.EnsureFilter(filter)
				filter.Expr[key] = cleanQueryValues(values)
			}
		}







|

|







105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
					sorter.Limit = limit
				}
			}
		case negateQKey:
			filter = place.EnsureFilter(filter)
			filter.Negate = true
		case sQKey:
			if vals := cleanQueryValues(values); len(vals) > 0 {
				filter = place.EnsureFilter(filter)
				filter.Expr[""] = vals
			}
		default:
			if !forSearch && meta.KeyIsValid(key) {
				filter = place.EnsureFilter(filter)
				filter.Expr[key] = cleanQueryValues(values)
			}
		}

Changes to web/adapter/urlbuilder.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// NewURLBuilder creates a new URLBuilder.
func NewURLBuilder(key byte) *URLBuilder {
	return &URLBuilder{key: key}
}

// Clone an URLBuilder
func (ub *URLBuilder) Clone() *URLBuilder {
	copy := new(URLBuilder)
	copy.key = ub.key
	if len(ub.path) > 0 {
		copy.path = make([]string, 0, len(ub.path))
	}
	for _, p := range ub.path {
		copy.path = append(copy.path, p)
	}
	if len(ub.query) > 0 {
		copy.query = make([]urlQuery, 0, len(ub.query))
	}
	for _, q := range ub.query {
		copy.query = append(copy.query, q)
	}
	copy.fragment = ub.fragment
	return copy
}

// SetZid sets the zettel identifier.
func (ub *URLBuilder) SetZid(zid id.Zid) *URLBuilder {
	if len(ub.path) > 0 {
		panic("Cannot add Zid")
	}
	ub.path = append(ub.path, zid.String())
	return ub
}

// AppendPath adds a new path element
func (ub *URLBuilder) AppendPath(p string) *URLBuilder {
	ub.path = append(ub.path, p)
	return ub
}

// AppendQuery adds a new query parameter
func (ub *URLBuilder) AppendQuery(key string, value string) *URLBuilder {
	ub.query = append(ub.query, urlQuery{key, value})
	return ub
}

// ClearQuery removes all query parameters.
func (ub *URLBuilder) ClearQuery() *URLBuilder {
	ub.query = nil







|
|

|
<
<
|


|
<
<
|

|
|


















|







32
33
34
35
36
37
38
39
40
41
42


43
44
45
46


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// NewURLBuilder creates a new URLBuilder.
func NewURLBuilder(key byte) *URLBuilder {
	return &URLBuilder{key: key}
}

// Clone an URLBuilder
func (ub *URLBuilder) Clone() *URLBuilder {
	cpy := new(URLBuilder)
	cpy.key = ub.key
	if len(ub.path) > 0 {
		cpy.path = make([]string, 0, len(ub.path))


		cpy.path = append(cpy.path, ub.path...)
	}
	if len(ub.query) > 0 {
		cpy.query = make([]urlQuery, 0, len(ub.query))


		cpy.query = append(cpy.query, ub.query...)
	}
	cpy.fragment = ub.fragment
	return cpy
}

// SetZid sets the zettel identifier.
func (ub *URLBuilder) SetZid(zid id.Zid) *URLBuilder {
	if len(ub.path) > 0 {
		panic("Cannot add Zid")
	}
	ub.path = append(ub.path, zid.String())
	return ub
}

// AppendPath adds a new path element
func (ub *URLBuilder) AppendPath(p string) *URLBuilder {
	ub.path = append(ub.path, p)
	return ub
}

// AppendQuery adds a new query parameter
func (ub *URLBuilder) AppendQuery(key, value string) *URLBuilder {
	ub.query = append(ub.query, urlQuery{key, value})
	return ub
}

// ClearQuery removes all query parameters.
func (ub *URLBuilder) ClearQuery() *URLBuilder {
	ub.query = nil

Changes to web/adapter/webui/create_zettel.go.

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
}

func renderZettelForm(
	w http.ResponseWriter,
	r *http.Request,
	te *TemplateEngine,
	zettel domain.Zettel,
	title string,
	heading string,
) {
	ctx := r.Context()
	user := session.GetUser(ctx)
	m := zettel.Meta
	var base baseData
	te.makeBaseData(ctx, runtime.GetLang(m), title, user, &base)
	te.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{







<
|







110
111
112
113
114
115
116

117
118
119
120
121
122
123
124
}

func renderZettelForm(
	w http.ResponseWriter,
	r *http.Request,
	te *TemplateEngine,
	zettel domain.Zettel,

	title, heading string,
) {
	ctx := r.Context()
	user := session.GetUser(ctx)
	m := zettel.Meta
	var base baseData
	te.makeBaseData(ctx, runtime.GetLang(m), title, user, &base)
	te.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{

Changes to web/adapter/webui/get_info.go.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
	"net/http"
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/session"
)

type metaDataInfo struct {
	Key   string
	Value string
}

type zettelReference struct {
	Zid    id.Zid
	Title  string
	HasURL bool
	URL    string
}

type matrixElement struct {
	Text   string
	HasURL bool
	URL    string
}
type matrixLine struct {
	Elements []matrixElement







<











<
<
<
<
<
<
<







16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31
32
33







34
35
36
37
38
39
40
	"net/http"
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/domain/id"

	"zettelstore.de/z/encoder"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/session"
)

type metaDataInfo struct {
	Key   string
	Value string
}








type matrixElement struct {
	Text   string
	HasURL bool
	URL    string
}
type matrixLine struct {
	Elements []matrixElement
116
117
118
119
120
121
122

123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145

146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
		user := session.GetUser(ctx)
		var base baseData
		te.makeBaseData(ctx, langOption.Value, textTitle, user, &base)
		canCopy := base.CanCreate && !zn.Zettel.Content.IsBinary()
		te.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct {
			Zid          string
			WebURL       string

			CanWrite     bool
			EditURL      string
			CanFolge     bool
			FolgeURL     string
			CanCopy      bool
			CopyURL      string
			CanNew       bool
			NewURL       string
			CanRename    bool
			RenameURL    string
			CanDelete    bool
			DeleteURL    string
			MetaData     []metaDataInfo
			HasLinks     bool
			HasLocLinks  bool
			LocLinks     []string
			HasExtLinks  bool
			ExtLinks     []string
			ExtNewWindow string
			Matrix       []matrixLine
		}{
			Zid:      zid.String(),
			WebURL:   adapter.NewURLBuilder('h').SetZid(zid).String(),

			CanWrite: te.canWrite(ctx, user, zn.Zettel),
			EditURL:  adapter.NewURLBuilder('e').SetZid(zid).String(),
			CanFolge: base.CanCreate && !zn.Zettel.Content.IsBinary(),
			FolgeURL: adapter.NewURLBuilder('f').SetZid(zid).String(),
			CanCopy:  canCopy,
			CopyURL:  adapter.NewURLBuilder('c').SetZid(zid).String(),
			CanNew: canCopy && zn.Zettel.Meta.GetDefault(meta.KeyRole, "") ==
				meta.ValueRoleNewTemplate,
			NewURL:       adapter.NewURLBuilder('n').SetZid(zid).String(),
			CanRename:    te.canRename(ctx, user, zn.Zettel.Meta),
			RenameURL:    adapter.NewURLBuilder('r').SetZid(zid).String(),
			CanDelete:    te.canDelete(ctx, user, zn.Zettel.Meta),
			DeleteURL:    adapter.NewURLBuilder('d').SetZid(zid).String(),
			MetaData:     metaData,
			HasLinks:     len(extLinks)+len(locLinks) > 0,
			HasLocLinks:  len(locLinks) > 0,
			LocLinks:     locLinks,
			HasExtLinks:  len(extLinks) > 0,
			ExtLinks:     extLinks,
			ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0),
			Matrix:       matrix,
		})
	}
}

func splitLocExtLinks(links []*ast.Reference) (locLinks []string, extLinks []string) {
	if len(links) == 0 {
		return nil, nil
	}
	for _, ref := range links {
		if ref.State == ast.RefStateZettelSelf {
			continue
		}
		if ref.IsZettel() {
			continue
		} else if ref.IsExternal() {
			extLinks = append(extLinks, ref.String())
		} else {
			locLinks = append(locLinks, ref.String())
		}
	}
	return locLinks, extLinks
}







>






<
<













|
|
>
|
|
|
|
|
|
<
<
<

|














|




|












108
109
110
111
112
113
114
115
116
117
118
119
120
121


122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
		user := session.GetUser(ctx)
		var base baseData
		te.makeBaseData(ctx, langOption.Value, textTitle, user, &base)
		canCopy := base.CanCreate && !zn.Zettel.Content.IsBinary()
		te.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
			HasLinks     bool
			HasLocLinks  bool
			LocLinks     []string
			HasExtLinks  bool
			ExtLinks     []string
			ExtNewWindow string
			Matrix       []matrixLine
		}{
			Zid:          zid.String(),
			WebURL:       adapter.NewURLBuilder('h').SetZid(zid).String(),
			ContextURL:   adapter.NewURLBuilder('j').SetZid(zid).String(),
			CanWrite:     te.canWrite(ctx, user, zn.Zettel),
			EditURL:      adapter.NewURLBuilder('e').SetZid(zid).String(),
			CanFolge:     base.CanCreate && !zn.Zettel.Content.IsBinary(),
			FolgeURL:     adapter.NewURLBuilder('f').SetZid(zid).String(),
			CanCopy:      canCopy,
			CopyURL:      adapter.NewURLBuilder('c').SetZid(zid).String(),



			CanRename:    te.canRename(ctx, user, zn.Zettel.Meta),
			RenameURL:    adapter.NewURLBuilder('b').SetZid(zid).String(),
			CanDelete:    te.canDelete(ctx, user, zn.Zettel.Meta),
			DeleteURL:    adapter.NewURLBuilder('d').SetZid(zid).String(),
			MetaData:     metaData,
			HasLinks:     len(extLinks)+len(locLinks) > 0,
			HasLocLinks:  len(locLinks) > 0,
			LocLinks:     locLinks,
			HasExtLinks:  len(extLinks) > 0,
			ExtLinks:     extLinks,
			ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0),
			Matrix:       matrix,
		})
	}
}

func splitLocExtLinks(links []*ast.Reference) (locLinks, extLinks []string) {
	if len(links) == 0 {
		return nil, nil
	}
	for _, ref := range links {
		if ref.State == ast.RefStateSelf {
			continue
		}
		if ref.IsZettel() {
			continue
		} else if ref.IsExternal() {
			extLinks = append(extLinks, ref.String())
		} else {
			locLinks = append(locLinks, ref.String())
		}
	}
	return locLinks, extLinks
}

Changes to web/adapter/webui/get_zettel.go.

105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
			InfoURL       string
			RoleText      string
			RoleURL       string
			HasTags       bool
			Tags          []simpleLink
			CanCopy       bool
			CopyURL       string
			CanNew        bool
			NewURL        string
			CanFolge      bool
			FolgeURL      string
			FolgeRefs     string
			PrecursorRefs string
			HasExtURL     bool
			ExtURL        string
			ExtNewWindow  string







<
<







105
106
107
108
109
110
111


112
113
114
115
116
117
118
			InfoURL       string
			RoleText      string
			RoleURL       string
			HasTags       bool
			Tags          []simpleLink
			CanCopy       bool
			CopyURL       string


			CanFolge      bool
			FolgeURL      string
			FolgeRefs     string
			PrecursorRefs string
			HasExtURL     bool
			ExtURL        string
			ExtNewWindow  string
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
			InfoURL:       adapter.NewURLBuilder('i').SetZid(zid).String(),
			RoleText:      roleText,
			RoleURL:       adapter.NewURLBuilder('h').AppendQuery("role", roleText).String(),
			HasTags:       len(tags) > 0,
			Tags:          tags,
			CanCopy:       canCopy,
			CopyURL:       adapter.NewURLBuilder('c').SetZid(zid).String(),
			CanNew:        canCopy && roleText == meta.ValueRoleNewTemplate,
			NewURL:        adapter.NewURLBuilder('n').SetZid(zid).String(),
			CanFolge:      base.CanCreate && !zn.Zettel.Content.IsBinary(),
			FolgeURL:      adapter.NewURLBuilder('f').SetZid(zid).String(),
			FolgeRefs:     formatMetaKey(zn.InhMeta, meta.KeyFolge, getTitle),
			PrecursorRefs: formatMetaKey(zn.InhMeta, meta.KeyPrecursor, getTitle),
			ExtURL:        extURL,
			HasExtURL:     hasExtURL,
			ExtNewWindow:  htmlAttrNewWindow(newWindow && hasExtURL),







<
<







127
128
129
130
131
132
133


134
135
136
137
138
139
140
			InfoURL:       adapter.NewURLBuilder('i').SetZid(zid).String(),
			RoleText:      roleText,
			RoleURL:       adapter.NewURLBuilder('h').AppendQuery("role", roleText).String(),
			HasTags:       len(tags) > 0,
			Tags:          tags,
			CanCopy:       canCopy,
			CopyURL:       adapter.NewURLBuilder('c').SetZid(zid).String(),


			CanFolge:      base.CanCreate && !zn.Zettel.Content.IsBinary(),
			FolgeURL:      adapter.NewURLBuilder('f').SetZid(zid).String(),
			FolgeRefs:     formatMetaKey(zn.InhMeta, meta.KeyFolge, getTitle),
			PrecursorRefs: formatMetaKey(zn.InhMeta, meta.KeyPrecursor, getTitle),
			ExtURL:        extURL,
			HasExtURL:     hasExtURL,
			ExtNewWindow:  htmlAttrNewWindow(newWindow && hasExtURL),
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
	}
	return content.String(), nil
}

func buildTagInfos(m *meta.Meta) []simpleLink {
	var tagInfos []simpleLink
	if tags, ok := m.GetList(meta.KeyTags); ok {
		tagInfos = make([]simpleLink, 0, len(tags))
		ub := adapter.NewURLBuilder('h')
		for _, t := range tags {
			// Cast to template.HTML is ok, because "t" is a tag name
			// and contains only legal characters by construction.
			tagInfos = append(
				tagInfos,
				simpleLink{Text: t, URL: ub.AppendQuery("tags", t).String()})
			ub.ClearQuery()
		}
	}
	return tagInfos
}

func formatMetaKey(m *meta.Meta, key string, getTitle getTitleFunc) string {







<

<
<
<
|
|
|







173
174
175
176
177
178
179

180



181
182
183
184
185
186
187
188
189
190
	}
	return content.String(), nil
}

func buildTagInfos(m *meta.Meta) []simpleLink {
	var tagInfos []simpleLink
	if tags, ok := m.GetList(meta.KeyTags); ok {

		ub := adapter.NewURLBuilder('h')



		tagInfos = make([]simpleLink, len(tags))
		for i, tag := range tags {
			tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery("tags", meta.CleanTag(tag)).String()}
			ub.ClearQuery()
		}
	}
	return tagInfos
}

func formatMetaKey(m *meta.Meta, key string, getTitle getTitleFunc) string {

Changes to web/adapter/webui/home.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

19
20



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35


36
37
38








39


40
41
42
43
44

45
46
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 webui provides wet-UI handlers for web requests.
package webui

import (
	"context"
	"net/http"

	"zettelstore.de/z/config/runtime"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"



)

type getRootStore interface {
	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
}

// MakeGetRootHandler creates a new HTTP handler to show the root URL.
func MakeGetRootHandler(
	s getRootStore, startNotFound, startFound http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/" {
			http.NotFound(w, r)
			return
		}


		startID := runtime.GetStart()
		if startID.IsValid() {
			if _, err := s.GetMeta(r.Context(), startID); err == nil {








				r.URL.Path = "/" + startID.String()


				startFound(w, r)
				return
			}
		}
		startNotFound(w, r)

	}
}

|
















>


>
>
>








|
<





>
>
|
|
|
>
>
>
>
>
>
>
>
|
>
>
|



<
>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

59
60
61
//-----------------------------------------------------------------------------
// Copyright (c) 2020-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 webui provides wet-UI handlers for web requests.
package webui

import (
	"context"
	"net/http"

	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/config/startup"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/place"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/session"
)

type getRootStore interface {
	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
}

// MakeGetRootHandler creates a new HTTP handler to show the root URL.
func MakeGetRootHandler(s getRootStore) http.HandlerFunc {

	return func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/" {
			http.NotFound(w, r)
			return
		}
		ok := false
		ctx := r.Context()
		homeZid := runtime.GetHomeZettel()
		if homeZid != id.DefaultHomeZid && homeZid.IsValid() {
			if _, err := s.GetMeta(ctx, homeZid); err != nil {
				homeZid = id.DefaultHomeZid
			} else {
				ok = true
			}
		}
		if !ok {
			if _, err := s.GetMeta(ctx, homeZid); err != nil {
				if place.IsErrNotAllowed(err) && startup.WithAuth() && session.GetUser(ctx) == nil {
					http.Redirect(w, r, adapter.NewURLBuilder('a').String(), http.StatusFound)
					return
				}
				http.Redirect(w, r, adapter.NewURLBuilder('h').String(), http.StatusFound)
				return
			}
		}

		http.Redirect(w, r, adapter.NewURLBuilder('h').SetZid(homeZid).String(), http.StatusFound)
	}
}

Changes to web/adapter/webui/htmlmeta.go.

63
64
65
66
67
68
69


70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
		writeWord(w, key, m.GetDefault(key, "???w"))
	case meta.TypeWordSet:
		if l, ok := m.GetList(key); ok {
			writeWordSet(w, key, l)
		}
	case meta.TypeZettelmarkup:
		writeZettelmarkup(w, m.GetDefault(key, "???z"), option)


	default:
		strfun.HTMLEscape(w, m.GetDefault(key, "???w"), false)
		fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key)
	}
}

func writeHTMLBool(w io.Writer, key string, val bool) {
	if val {
		writeLink(w, key, "True")
	} else {
		writeLink(w, key, "False")
	}
}

func writeCredential(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}








>
>








|

|







63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
		writeWord(w, key, m.GetDefault(key, "???w"))
	case meta.TypeWordSet:
		if l, ok := m.GetList(key); ok {
			writeWordSet(w, key, l)
		}
	case meta.TypeZettelmarkup:
		writeZettelmarkup(w, m.GetDefault(key, "???z"), option)
	case meta.TypeUnknown:
		writeUnknown(w, m.GetDefault(key, "???u"))
	default:
		strfun.HTMLEscape(w, m.GetDefault(key, "???w"), false)
		fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key)
	}
}

func writeHTMLBool(w io.Writer, key string, val bool) {
	if val {
		writeLink(w, key, "true", "True")
	} else {
		writeLink(w, key, "false", "False")
	}
}

func writeCredential(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}

128
129
130
131
132
133
134




135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
func writeNumber(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}

func writeString(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}





func writeTagSet(w io.Writer, key string, tags []string) {
	for i, tag := range tags {
		if i > 0 {
			w.Write(space)
		}
		writeLink(w, key, tag)
	}
}

func writeTimestamp(w io.Writer, ts time.Time) {
	io.WriteString(w, ts.Format("2006-01-02&nbsp;15:04:05"))
}

func writeURL(w io.Writer, val string) {
	u, err := url.Parse(val)
	if err != nil {
		strfun.HTMLEscape(w, val, false)
		return
	}
	fmt.Fprintf(w, "<a href=\"%v\">", u)
	strfun.HTMLEscape(w, val, false)
	io.WriteString(w, "</a>")
}

func writeWord(w io.Writer, key, word string) {
	writeLink(w, key, word)
}

func writeWordSet(w io.Writer, key string, words []string) {
	for i, word := range words {
		if i > 0 {
			w.Write(space)
		}
		writeWord(w, key, word)
	}
}
func writeZettelmarkup(w io.Writer, val string, option encoder.Option) {
	astTitle := parser.ParseTitle(val)
	title, err := adapter.FormatInlines(astTitle, "html", option)
	if err != nil {
		strfun.HTMLEscape(w, val, false)
		return
	}
	io.WriteString(w, title)
}

func writeLink(w io.Writer, key, value string) {
	fmt.Fprintf(
		w, "<a href=\"%v?%v=%v\">",
		adapter.NewURLBuilder('h'), url.QueryEscape(key), url.QueryEscape(value))
	strfun.HTMLEscape(w, value, false)
	io.WriteString(w, "</a>")
}

type getTitleFunc func(id.Zid, string) (string, int)

func makeGetTitle(ctx context.Context, getMeta usecase.GetMeta, langOption encoder.Option) getTitleFunc {
	return func(zid id.Zid, format string) (string, int) {







>
>
>
>






|



















|




















|



|







130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
func writeNumber(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}

func writeString(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}

func writeUnknown(w io.Writer, val string) {
	strfun.HTMLEscape(w, val, false)
}

func writeTagSet(w io.Writer, key string, tags []string) {
	for i, tag := range tags {
		if i > 0 {
			w.Write(space)
		}
		writeLink(w, key, meta.CleanTag(tag), tag)
	}
}

func writeTimestamp(w io.Writer, ts time.Time) {
	io.WriteString(w, ts.Format("2006-01-02&nbsp;15:04:05"))
}

func writeURL(w io.Writer, val string) {
	u, err := url.Parse(val)
	if err != nil {
		strfun.HTMLEscape(w, val, false)
		return
	}
	fmt.Fprintf(w, "<a href=\"%v\">", u)
	strfun.HTMLEscape(w, val, false)
	io.WriteString(w, "</a>")
}

func writeWord(w io.Writer, key, word string) {
	writeLink(w, key, word, word)
}

func writeWordSet(w io.Writer, key string, words []string) {
	for i, word := range words {
		if i > 0 {
			w.Write(space)
		}
		writeWord(w, key, word)
	}
}
func writeZettelmarkup(w io.Writer, val string, option encoder.Option) {
	astTitle := parser.ParseTitle(val)
	title, err := adapter.FormatInlines(astTitle, "html", option)
	if err != nil {
		strfun.HTMLEscape(w, val, false)
		return
	}
	io.WriteString(w, title)
}

func writeLink(w io.Writer, key, value, text string) {
	fmt.Fprintf(
		w, "<a href=\"%v?%v=%v\">",
		adapter.NewURLBuilder('h'), url.QueryEscape(key), url.QueryEscape(value))
	strfun.HTMLEscape(w, text, false)
	io.WriteString(w, "</a>")
}

type getTitleFunc func(id.Zid, string) (string, int)

func makeGetTitle(ctx context.Context, getMeta usecase.GetMeta, langOption encoder.Option) getTitleFunc {
	return func(zid id.Zid, format string) (string, int) {

Changes to web/adapter/webui/lists.go.

13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60


61
62
63
64
65
66
67
68
69

70
71
72
73
74
75
76
77
78

import (
	"context"
	"net/http"
	"net/url"
	"sort"
	"strconv"


	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/index"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/session"
)

// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML.
func MakeListHTMLMetaHandler(
	te *TemplateEngine, listMeta usecase.ListMeta) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		renderWebUIZettelList(w, r, te, listMeta)
	}
}

// MakeWebUIListsHandler creates a new HTTP handler for the use case "list some zettel".
func MakeWebUIListsHandler(
	te *TemplateEngine,
	listMeta usecase.ListMeta,
	listRole usecase.ListRole,
	listTags usecase.ListTags,
) 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
		}
		switch zid {
		case 1:
			renderWebUIZettelList(w, r, te, listMeta)
		case 2:
			renderWebUIRolesList(w, r, te, listRole)
		case 3:
			renderWebUITagsList(w, r, te, listTags)


		}
	}
}

func renderWebUIZettelList(
	w http.ResponseWriter, r *http.Request, te *TemplateEngine, listMeta usecase.ListMeta) {
	query := r.URL.Query()
	filter, sorter := adapter.GetFilterSorter(query, false)
	ctx := r.Context()

	renderWebUIMetaList(
		ctx, w, te, sorter,
		func(sorter *place.Sorter) ([]*meta.Meta, error) {
			if filter == nil && (sorter == nil || sorter.Order == "") {
				ctx = index.NoEnrichContext(ctx)
			}
			return listMeta.Run(ctx, filter, sorter)
		},
		func(offset int) string {







>















<
<
<
<
<
<
<
<






|
<
<
<
<
|
|
<
<

|

>
>









>

|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35








36
37
38
39
40
41
42




43
44


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

import (
	"context"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"

	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/index"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/place"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/session"
)

// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML.
func MakeListHTMLMetaHandler(








	te *TemplateEngine,
	listMeta usecase.ListMeta,
	listRole usecase.ListRole,
	listTags usecase.ListTags,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		query := r.URL.Query()




		switch query.Get("_l") {
		case "r":


			renderWebUIRolesList(w, r, te, listRole)
		case "t":
			renderWebUITagsList(w, r, te, listTags)
		default:
			renderWebUIZettelList(w, r, te, listMeta)
		}
	}
}

func renderWebUIZettelList(
	w http.ResponseWriter, r *http.Request, te *TemplateEngine, listMeta usecase.ListMeta) {
	query := r.URL.Query()
	filter, sorter := adapter.GetFilterSorter(query, false)
	ctx := r.Context()
	title := listTitleFilterSorter("Filter", filter, sorter)
	renderWebUIMetaList(
		ctx, w, te, title, sorter,
		func(sorter *place.Sorter) ([]*meta.Meta, error) {
			if filter == nil && (sorter == nil || sorter.Order == "") {
				ctx = index.NoEnrichContext(ctx)
			}
			return listMeta.Run(ctx, filter, sorter)
		},
		func(offset int) string {
201
202
203
204
205
206
207

208
209
210
211
212
213
214
215
216
217
218
219
220
221
































































222
223

224
225
226
227
228
229
230
		filter, sorter := adapter.GetFilterSorter(query, true)
		if filter == nil || len(filter.Expr) == 0 {
			http.Redirect(w, r, adapter.NewURLBuilder('h').String(), http.StatusFound)
			return
		}

		ctx := r.Context()

		renderWebUIMetaList(
			ctx, w, te, sorter,
			func(sorter *place.Sorter) ([]*meta.Meta, error) {
				if filter == nil && (sorter == nil || sorter.Order == "") {
					ctx = index.NoEnrichContext(ctx)
				}
				return search.Run(ctx, filter, sorter)
			},
			func(offset int) string {
				return newPageURL('s', query, offset, "offset", "limit")
			})
	}
}

































































func renderWebUIMetaList(
	ctx context.Context, w http.ResponseWriter, te *TemplateEngine,

	sorter *place.Sorter,
	ucMetaList func(sorter *place.Sorter) ([]*meta.Meta, error),
	pageURL func(int) string) {

	var metaList []*meta.Meta
	var err error
	var prevURL, nextURL string







>

|







|




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


>







191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
		filter, sorter := adapter.GetFilterSorter(query, true)
		if filter == nil || len(filter.Expr) == 0 {
			http.Redirect(w, r, adapter.NewURLBuilder('h').String(), http.StatusFound)
			return
		}

		ctx := r.Context()
		title := listTitleFilterSorter("Search", filter, sorter)
		renderWebUIMetaList(
			ctx, w, te, title, sorter,
			func(sorter *place.Sorter) ([]*meta.Meta, error) {
				if filter == nil && (sorter == nil || sorter.Order == "") {
					ctx = index.NoEnrichContext(ctx)
				}
				return search.Run(ctx, filter, sorter)
			},
			func(offset int) string {
				return newPageURL('f', query, offset, "offset", "limit")
			})
	}
}

// MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context".
func MakeZettelContextHandler(te *TemplateEngine, 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 := usecase.ParseZCDirection(q.Get("dir"))
		depth, ok := adapter.GetInteger(q, "depth")
		if !ok || depth < 0 {
			depth = 5
		}
		limit, ok := adapter.GetInteger(q, "limit")
		if !ok || limit < 0 {
			limit = 200
		}
		ctx := r.Context()
		metaList, err := getContext.Run(ctx, zid, dir, depth, limit)
		if err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		metaLinks, err := buildHTMLMetaList(metaList)
		if err != nil {
			adapter.InternalServerError(w, "Build HTML meta list", err)
			return
		}

		depths := []string{"2", "3", "4", "5", "6", "7", "8", "9", "10"}
		depthLinks := make([]simpleLink, len(depths))
		depthURL := adapter.NewURLBuilder('j').SetZid(zid)
		for i, depth := range depths {
			depthURL.ClearQuery()
			switch dir {
			case usecase.ZettelContextBackward:
				depthURL.AppendQuery("dir", "backward")
			case usecase.ZettelContextForward:
				depthURL.AppendQuery("dir", "forward")
			}
			depthURL.AppendQuery("depth", depth)
			depthLinks[i].Text = depth
			depthLinks[i].URL = depthURL.String()
		}
		var base baseData
		user := session.GetUser(ctx)
		te.makeBaseData(ctx, runtime.GetDefaultLang(), runtime.GetSiteName(), user, &base)
		te.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct {
			Title   string
			InfoURL string
			Depths  []simpleLink
			Start   simpleLink
			Metas   []simpleLink
		}{
			Title:   "Zettel Context",
			InfoURL: adapter.NewURLBuilder('i').SetZid(zid).String(),
			Depths:  depthLinks,
			Start:   metaLinks[0],
			Metas:   metaLinks[1:],
		})
	}
}

func renderWebUIMetaList(
	ctx context.Context, w http.ResponseWriter, te *TemplateEngine,
	title string,
	sorter *place.Sorter,
	ucMetaList func(sorter *place.Sorter) ([]*meta.Meta, error),
	pageURL func(int) string) {

	var metaList []*meta.Meta
	var err error
	var prevURL, nextURL string
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285


































































































286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
		adapter.InternalServerError(w, "Build HTML meta list", err)
		return
	}
	var base baseData
	te.makeBaseData(ctx, runtime.GetDefaultLang(), runtime.GetSiteName(), user, &base)
	te.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct {
		Title       string
		Metas       []metaInfo
		HasPrevNext bool
		HasPrev     bool
		PrevURL     string
		HasNext     bool
		NextURL     string
	}{
		Title:       base.Title,
		Metas:       metas,
		HasPrevNext: len(prevURL) > 0 || len(nextURL) > 0,
		HasPrev:     len(prevURL) > 0,
		PrevURL:     prevURL,
		HasNext:     len(nextURL) > 0,
		NextURL:     nextURL,
	})
}



































































































func newPageURL(key byte, query url.Values, offset int, offsetKey, limitKey string) string {
	urlBuilder := adapter.NewURLBuilder(key)
	for key, values := range query {
		if key != offsetKey && key != limitKey {
			for _, val := range values {
				urlBuilder.AppendQuery(key, val)
			}
		}
	}
	if offset > 0 {
		urlBuilder.AppendQuery(offsetKey, strconv.Itoa(offset))
	}
	return urlBuilder.String()
}

type metaInfo struct {
	Title string
	URL   string
}

// buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering.
func buildHTMLMetaList(metaList []*meta.Meta) ([]metaInfo, error) {
	defaultLang := runtime.GetDefaultLang()
	langOption := encoder.StringOption{Key: "lang", Value: ""}
	metas := make([]metaInfo, 0, len(metaList))
	for _, m := range metaList {
		if lang, ok := m.Get(meta.KeyLang); ok {
			langOption.Value = lang
		} else {
			langOption.Value = defaultLang
		}
		title, _ := m.Get(meta.KeyTitle)
		htmlTitle, err := adapter.FormatInlines(
			parser.ParseTitle(title), "html", &langOption)
		if err != nil {
			return nil, err
		}
		metas = append(metas, metaInfo{
			Title: htmlTitle,
			URL:   adapter.NewURLBuilder('h').SetZid(m.Zid).String(),
		})
	}
	return metas, nil
}







|






|








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
















<
<
<
<
<

|


|












|
|
|




319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455





456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
		adapter.InternalServerError(w, "Build HTML meta list", err)
		return
	}
	var base baseData
	te.makeBaseData(ctx, runtime.GetDefaultLang(), runtime.GetSiteName(), user, &base)
	te.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct {
		Title       string
		Metas       []simpleLink
		HasPrevNext bool
		HasPrev     bool
		PrevURL     string
		HasNext     bool
		NextURL     string
	}{
		Title:       title,
		Metas:       metas,
		HasPrevNext: len(prevURL) > 0 || len(nextURL) > 0,
		HasPrev:     len(prevURL) > 0,
		PrevURL:     prevURL,
		HasNext:     len(nextURL) > 0,
		NextURL:     nextURL,
	})
}

func listTitleFilterSorter(prefix string, filter *place.Filter, sorter *place.Sorter) string {
	if filter == nil && sorter == nil {
		return runtime.GetSiteName()
	}
	var sb strings.Builder
	sb.WriteString(prefix)
	sb.WriteString(": ")
	if filter != nil {
		listTitleFilter(&sb, filter)
		if sorter != nil {
			sb.WriteString(" | ")
			listTitleSorter(&sb, sorter)
		}
	} else if sorter != nil {
		listTitleSorter(&sb, sorter)
	}
	return sb.String()
}

func listTitleFilter(sb *strings.Builder, filter *place.Filter) {
	if filter.Negate {
		sb.WriteString("NOT (")
	}
	names := make([]string, 0, len(filter.Expr))
	for name := range filter.Expr {
		names = append(names, name)
	}
	sort.Strings(names)
	for i, name := range names {
		if i > 0 {
			sb.WriteString(" AND ")
		}
		if name == "" {
			sb.WriteString("ANY")
		} else {
			sb.WriteString(name)
		}
		sb.WriteString(" MATCH ")
		writeFilterExprValues(sb, filter.Expr[name])
	}
	if filter.Negate {
		sb.WriteByte(')')
	}
}

func writeFilterExprValues(sb *strings.Builder, values []string) {
	if len(values) == 0 {
		sb.WriteString("ANY")
		return
	}

	for j, val := range values {
		if j > 0 {
			sb.WriteString(" AND ")
		}
		if val == "" {
			sb.WriteString("ANY")
		} else {
			sb.WriteString(val)
		}
	}
}

func listTitleSorter(sb *strings.Builder, sorter *place.Sorter) {
	var space bool
	if ord := sorter.Order; len(ord) > 0 {
		switch ord {
		case meta.KeyID:
			// Ignore
		case place.RandomOrder:
			sb.WriteString("RANDOM")
			space = true
		default:
			sb.WriteString("SORT ")
			sb.WriteString(ord)
			if sorter.Descending {
				sb.WriteString(" DESC")
			}
			space = true
		}
	}
	if off := sorter.Offset; off > 0 {
		if space {
			sb.WriteByte(' ')
		}
		sb.WriteString("OFFSET ")
		sb.WriteString(strconv.Itoa(off))
		space = true
	}
	if lim := sorter.Limit; lim > 0 {
		if space {
			sb.WriteByte(' ')
		}
		sb.WriteString("LIMIT ")
		sb.WriteString(strconv.Itoa(lim))
	}
}

func newPageURL(key byte, query url.Values, offset int, offsetKey, limitKey string) string {
	urlBuilder := adapter.NewURLBuilder(key)
	for key, values := range query {
		if key != offsetKey && key != limitKey {
			for _, val := range values {
				urlBuilder.AppendQuery(key, val)
			}
		}
	}
	if offset > 0 {
		urlBuilder.AppendQuery(offsetKey, strconv.Itoa(offset))
	}
	return urlBuilder.String()
}






// buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering.
func buildHTMLMetaList(metaList []*meta.Meta) ([]simpleLink, error) {
	defaultLang := runtime.GetDefaultLang()
	langOption := encoder.StringOption{Key: "lang", Value: ""}
	metas := make([]simpleLink, 0, len(metaList))
	for _, m := range metaList {
		if lang, ok := m.Get(meta.KeyLang); ok {
			langOption.Value = lang
		} else {
			langOption.Value = defaultLang
		}
		title, _ := m.Get(meta.KeyTitle)
		htmlTitle, err := adapter.FormatInlines(
			parser.ParseTitle(title), "html", &langOption)
		if err != nil {
			return nil, err
		}
		metas = append(metas, simpleLink{
			Text: htmlTitle,
			URL:  adapter.NewURLBuilder('h').SetZid(m.Zid).String(),
		})
	}
	return metas, nil
}

Deleted web/adapter/webui/reload.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 webui provides wet-UI handlers for web requests.
package webui

import (
	"net/http"

	"zettelstore.de/z/web/adapter"
)

// ReloadHandlerHTML creates a new HTTP handler for the use case "reload".
func ReloadHandlerHTML(w http.ResponseWriter, r *http.Request) {
	http.Redirect(w, r, adapter.NewURLBuilder('/').String(), http.StatusFound)
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































Changes to web/adapter/webui/rename_zettel.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
func MakePostRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		curZid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}
		if err := r.ParseForm(); err != nil {
			adapter.BadRequest(w, "Unable to read rename zettel form")
			return
		}
		if formCurZid, err := id.Parse(
			r.PostFormValue("curzid")); err != nil || formCurZid != curZid {
			adapter.BadRequest(w, "Invalid value for current zettel id in form")
			return
		}
		newZid, err := id.Parse(strings.TrimSpace(r.PostFormValue("newzid")))
		if err != nil {
			adapter.BadRequest(w, fmt.Sprintf("Invalid new zettel id %q", newZid.String()))
			return
		}

		if err := renameZettel.Run(r.Context(), curZid, newZid); err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}
		http.Redirect(
			w, r, adapter.NewURLBuilder('h').SetZid(newZid).String(), http.StatusFound)
	}
}







|



|
|













<
|


64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

90
91
92
func MakePostRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		curZid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}
		if err = r.ParseForm(); err != nil {
			adapter.BadRequest(w, "Unable to read rename zettel form")
			return
		}
		if formCurZid, err1 := id.Parse(
			r.PostFormValue("curzid")); err1 != nil || formCurZid != curZid {
			adapter.BadRequest(w, "Invalid value for current zettel id in form")
			return
		}
		newZid, err := id.Parse(strings.TrimSpace(r.PostFormValue("newzid")))
		if err != nil {
			adapter.BadRequest(w, fmt.Sprintf("Invalid new zettel id %q", newZid.String()))
			return
		}

		if err := renameZettel.Run(r.Context(), curZid, newZid); err != nil {
			adapter.ReportUsecaseError(w, err)
			return
		}

		http.Redirect(w, r, adapter.NewURLBuilder('h').SetZid(newZid).String(), http.StatusFound)
	}
}

Changes to web/adapter/webui/template.go.

15
16
17
18
19
20
21

22
23
24
25
26
27
28
	"bytes"
	"context"
	"net/http"
	"sync"

	"zettelstore.de/z/auth/policy"
	"zettelstore.de/z/auth/token"

	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/config/startup"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/index"







>







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
	"bytes"
	"context"
	"net/http"
	"sync"

	"zettelstore.de/z/auth/policy"
	"zettelstore.de/z/auth/token"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config/runtime"
	"zettelstore.de/z/config/startup"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/index"
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
	stylesheetURL string
	homeURL       string
	listZettelURL string
	listRolesURL  string
	listTagsURL   string
	withAuth      bool
	loginURL      string
	reloadURL     string
	searchURL     string
}

// NewTemplateEngine creates a new TemplateEngine.
func NewTemplateEngine(mgr place.Manager, pol policy.Policy) *TemplateEngine {
	te := &TemplateEngine{
		place:  mgr,
		policy: pol,

		stylesheetURL: adapter.NewURLBuilder('z').SetZid(
			id.BaseCSSZid).AppendQuery("_format", "raw").AppendQuery(
			"_part", "content").String(),
		homeURL:       adapter.NewURLBuilder('/').String(),
		listZettelURL: adapter.NewURLBuilder('h').String(),
		listRolesURL:  adapter.NewURLBuilder('k').SetZid(2).String(),
		listTagsURL:   adapter.NewURLBuilder('k').SetZid(3).String(),
		withAuth:      startup.WithAuth(),
		loginURL:      adapter.NewURLBuilder('a').String(),
		reloadURL:     adapter.NewURLBuilder('c').AppendQuery("_format", "html").String(),
		searchURL:     adapter.NewURLBuilder('s').String(),
	}
	te.observe(place.ChangeInfo{Reason: place.OnReload, Zid: id.Invalid})
	mgr.RegisterObserver(te.observe)
	return te
}

func (te *TemplateEngine) observe(ci place.ChangeInfo) {







<














|
|


<
|







55
56
57
58
59
60
61

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

80
81
82
83
84
85
86
87
	stylesheetURL string
	homeURL       string
	listZettelURL string
	listRolesURL  string
	listTagsURL   string
	withAuth      bool
	loginURL      string

	searchURL     string
}

// NewTemplateEngine creates a new TemplateEngine.
func NewTemplateEngine(mgr place.Manager, pol policy.Policy) *TemplateEngine {
	te := &TemplateEngine{
		place:  mgr,
		policy: pol,

		stylesheetURL: adapter.NewURLBuilder('z').SetZid(
			id.BaseCSSZid).AppendQuery("_format", "raw").AppendQuery(
			"_part", "content").String(),
		homeURL:       adapter.NewURLBuilder('/').String(),
		listZettelURL: adapter.NewURLBuilder('h').String(),
		listRolesURL:  adapter.NewURLBuilder('h').AppendQuery("_l", "r").String(),
		listTagsURL:   adapter.NewURLBuilder('h').AppendQuery("_l", "t").String(),
		withAuth:      startup.WithAuth(),
		loginURL:      adapter.NewURLBuilder('a').String(),

		searchURL:     adapter.NewURLBuilder('f').String(),
	}
	te.observe(place.ChangeInfo{Reason: place.OnReload, Zid: id.Invalid})
	mgr.RegisterObserver(te.observe)
	return te
}

func (te *TemplateEngine) observe(ci place.ChangeInfo) {
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142

143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170



171
172

173
174
175
176
177
178
179
180
181
182
183
184
185
186

func (te *TemplateEngine) canWrite(
	ctx context.Context, user *meta.Meta, zettel domain.Zettel) bool {
	return te.policy.CanWrite(user, zettel.Meta, zettel.Meta) &&
		te.place.CanUpdateZettel(ctx, zettel)
}

func (te *TemplateEngine) canRename(
	ctx context.Context, user *meta.Meta, m *meta.Meta) bool {
	return te.policy.CanRename(user, m) && te.place.AllowRenameZettel(ctx, m.Zid)
}

func (te *TemplateEngine) canDelete(
	ctx context.Context, user *meta.Meta, m *meta.Meta) bool {
	return te.policy.CanDelete(user, m) && te.place.CanDeleteZettel(ctx, m.Zid)
}

func (te *TemplateEngine) getTemplate(
	ctx context.Context, templateID id.Zid) (*template.Template, error) {
	if t, ok := te.cacheGetTemplate(templateID); ok {
		return t, nil
	}
	realTemplateZettel, err := te.place.GetZettel(ctx, templateID)
	if err != nil {
		return nil, err
	}
	t, err := template.ParseString(realTemplateZettel.Content.AsString(), nil)
	if err == nil {

		te.cacheSetTemplate(templateID, t)
	}
	return t, err
}

type simpleLink struct {
	Text string
	URL  string
}

type baseData struct {
	Lang           string
	MetaHeader     string
	StylesheetURL  string
	Title          string
	HomeURL        string
	ListZettelURL  string
	ListRolesURL   string
	ListTagsURL    string
	CanCreate      bool
	NewZettelURL   string
	NewZettelLinks []simpleLink
	WithAuth       bool
	UserIsValid    bool
	UserZettelURL  string
	UserIdent      string
	UserLogoutURL  string
	LoginURL       string



	CanReload      bool
	ReloadURL      string

	SearchURL      string
	Content        string
	FooterHTML     string
}

func (te *TemplateEngine) makeBaseData(
	ctx context.Context, lang string, title string, user *meta.Meta, data *baseData) {
	var (
		newZettelLinks []simpleLink
		userZettelURL  string
		userIdent      string
		userLogoutURL  string
	)
	canCreate := te.canCreate(ctx, user)







|
<



|
<














>
















<
<
<
|
<
<






>
>
>
|
|
>






|







114
115
116
117
118
119
120
121

122
123
124
125

126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156



157


158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183

func (te *TemplateEngine) canWrite(
	ctx context.Context, user *meta.Meta, zettel domain.Zettel) bool {
	return te.policy.CanWrite(user, zettel.Meta, zettel.Meta) &&
		te.place.CanUpdateZettel(ctx, zettel)
}

func (te *TemplateEngine) canRename(ctx context.Context, user, m *meta.Meta) bool {

	return te.policy.CanRename(user, m) && te.place.AllowRenameZettel(ctx, m.Zid)
}

func (te *TemplateEngine) canDelete(ctx context.Context, user, m *meta.Meta) bool {

	return te.policy.CanDelete(user, m) && te.place.CanDeleteZettel(ctx, m.Zid)
}

func (te *TemplateEngine) getTemplate(
	ctx context.Context, templateID id.Zid) (*template.Template, error) {
	if t, ok := te.cacheGetTemplate(templateID); ok {
		return t, nil
	}
	realTemplateZettel, err := te.place.GetZettel(ctx, templateID)
	if err != nil {
		return nil, err
	}
	t, err := template.ParseString(realTemplateZettel.Content.AsString(), nil)
	if err == nil {
		// t.SetErrorOnMissing()
		te.cacheSetTemplate(templateID, t)
	}
	return t, err
}

type simpleLink struct {
	Text string
	URL  string
}

type baseData struct {
	Lang           string
	MetaHeader     string
	StylesheetURL  string
	Title          string
	HomeURL        string



	WithUser       bool


	WithAuth       bool
	UserIsValid    bool
	UserZettelURL  string
	UserIdent      string
	UserLogoutURL  string
	LoginURL       string
	ListZettelURL  string
	ListRolesURL   string
	ListTagsURL    string
	CanCreate      bool
	NewZettelURL   string
	NewZettelLinks []simpleLink
	SearchURL      string
	Content        string
	FooterHTML     string
}

func (te *TemplateEngine) makeBaseData(
	ctx context.Context, lang, title string, user *meta.Meta, data *baseData) {
	var (
		newZettelLinks []simpleLink
		userZettelURL  string
		userIdent      string
		userLogoutURL  string
	)
	canCreate := te.canCreate(ctx, user)
194
195
196
197
198
199
200
201
202
203
204
205
206

207
208
209
210
211
212

213


214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245


246
247








248


249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
		userLogoutURL = adapter.NewURLBuilder('a').SetZid(user.Zid).String()
	}

	data.Lang = lang
	data.StylesheetURL = te.stylesheetURL
	data.Title = title
	data.HomeURL = te.homeURL
	data.ListZettelURL = te.listZettelURL
	data.ListRolesURL = te.listRolesURL
	data.ListTagsURL = te.listTagsURL
	data.CanCreate = canCreate
	data.NewZettelLinks = newZettelLinks
	data.WithAuth = te.withAuth

	data.UserIsValid = userIsValid
	data.UserZettelURL = userZettelURL
	data.UserIdent = userIdent
	data.UserLogoutURL = userLogoutURL
	data.LoginURL = te.loginURL
	data.CanReload = te.policy.CanReload(user)

	data.ReloadURL = te.reloadURL


	data.SearchURL = te.searchURL
	data.FooterHTML = runtime.GetFooterHTML()
}

// htmlAttrNewWindow eturns 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 ""
}

var templatePlaceFilter = &place.Filter{
	Expr: place.FilterExpr{
		meta.KeyRole: []string{meta.ValueRoleNewTemplate},
	},
}

var templatePlaceSorter = &place.Sorter{
	Order:      "id",
	Descending: false,
	Offset:     -1,
	Limit:      31, // Just to be one the safe side...
}

func (te *TemplateEngine) fetchNewTemplates(ctx context.Context, user *meta.Meta) []simpleLink {
	ctx = index.NoEnrichContext(ctx)
	templateList, err := te.place.SelectMeta(ctx, templatePlaceFilter, templatePlaceSorter)
	if err != nil {
		return nil
	}


	result := make([]simpleLink, 0, len(templateList))
	for _, m := range templateList {








		if te.policy.CanRead(user, m) {


			title := runtime.GetTitle(m)
			langOption := encoder.StringOption{Key: "lang", Value: runtime.GetLang(m)}
			astTitle := parser.ParseInlines(
				input.NewInput(runtime.GetTitle(m)), meta.ValueSyntaxZmk)
			menuTitle, err := adapter.FormatInlines(astTitle, "html", &langOption)
			if err != nil {
				menuTitle, err = adapter.FormatInlines(astTitle, "text", &langOption)
				if err != nil {
					menuTitle = title
				}
			}
			result = append(result, simpleLink{
				Text: menuTitle,
				URL:  adapter.NewURLBuilder('n').SetZid(m.Zid).String(),
			})
		}
	}
	return result
}

func (te *TemplateEngine) renderTemplate(
	ctx context.Context,
	w http.ResponseWriter,







<
<
<
<
<

>





|
>
|
>
>













<
<
<
<
<
<
<
<
<
<
<
<
<


|



>
>
|
|
>
>
>
>
>
>
>
>
|
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<







191
192
193
194
195
196
197





198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222













223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258

259
260
261
262
263
264
265
		userLogoutURL = adapter.NewURLBuilder('a').SetZid(user.Zid).String()
	}

	data.Lang = lang
	data.StylesheetURL = te.stylesheetURL
	data.Title = title
	data.HomeURL = te.homeURL





	data.WithAuth = te.withAuth
	data.WithUser = data.WithAuth
	data.UserIsValid = userIsValid
	data.UserZettelURL = userZettelURL
	data.UserIdent = userIdent
	data.UserLogoutURL = userLogoutURL
	data.LoginURL = te.loginURL
	data.ListZettelURL = te.listZettelURL
	data.ListRolesURL = te.listRolesURL
	data.ListTagsURL = te.listTagsURL
	data.CanCreate = canCreate
	data.NewZettelLinks = newZettelLinks
	data.SearchURL = te.searchURL
	data.FooterHTML = runtime.GetFooterHTML()
}

// htmlAttrNewWindow eturns 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 (te *TemplateEngine) fetchNewTemplates(ctx context.Context, user *meta.Meta) []simpleLink {
	ctx = index.NoEnrichContext(ctx)
	menu, err := te.place.GetZettel(ctx, id.TOCNewTemplateZid)
	if err != nil {
		return nil
	}
	zn := parser.ParseZettel(menu, "")
	refs := collect.Order(zn)
	result := make([]simpleLink, 0, len(refs))
	for _, ref := range refs {
		zid, err := id.Parse(ref.URL.Path)
		if err != nil {
			continue
		}
		m, err := te.place.GetMeta(ctx, zid)
		if err != nil {
			continue
		}
		if !te.policy.CanRead(user, m) {
			continue
		}
		title := runtime.GetTitle(m)
		langOption := encoder.StringOption{Key: "lang", Value: runtime.GetLang(m)}
		astTitle := parser.ParseInlines(
			input.NewInput(runtime.GetTitle(m)), meta.ValueSyntaxZmk)
		menuTitle, err := adapter.FormatInlines(astTitle, "html", &langOption)
		if err != nil {
			menuTitle, err = adapter.FormatInlines(astTitle, "text", &langOption)
			if err != nil {
				menuTitle = title
			}
		}
		result = append(result, simpleLink{
			Text: menuTitle,
			URL:  adapter.NewURLBuilder('g').SetZid(m.Zid).String(),
		})

	}
	return result
}

func (te *TemplateEngine) renderTemplate(
	ctx context.Context,
	w http.ResponseWriter,
281
282
283
284
285
286
287
288
289
290
291
292
293
294

295
296
297

298
299
300
301
	t, err := te.getTemplate(ctx, templateID)
	if err != nil {
		adapter.InternalServerError(w, "Unable to get template", err)
		return
	}
	if user := session.GetUser(ctx); user != nil {
		htmlLifetime, _ := startup.TokenLifetime()
		t, err := token.GetToken(user, htmlLifetime, token.KindHTML)
		if err == nil {
			session.SetToken(w, t, htmlLifetime)
		}
	}
	var content bytes.Buffer
	err = t.Render(&content, data)

	base.Content = content.String()
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	err = bt.Render(w, base)

	if err != nil {
		adapter.InternalServerError(w, "Unable to render template", err)
	}
}







|
<
|




>
|
|
|
>




275
276
277
278
279
280
281
282

283
284
285
286
287
288
289
290
291
292
293
294
295
296
	t, err := te.getTemplate(ctx, templateID)
	if err != nil {
		adapter.InternalServerError(w, "Unable to get template", err)
		return
	}
	if user := session.GetUser(ctx); user != nil {
		htmlLifetime, _ := startup.TokenLifetime()
		if tok, err1 := token.GetToken(user, htmlLifetime, token.KindHTML); err1 == nil {

			session.SetToken(w, tok, htmlLifetime)
		}
	}
	var content bytes.Buffer
	err = t.Render(&content, data)
	if err == nil {
		base.Content = content.String()
		w.Header().Set(adapter.ContentType, "text/html; charset=utf-8")
		err = bt.Render(w, base)
	}
	if err != nil {
		adapter.InternalServerError(w, "Unable to render template", err)
	}
}

Changes to web/router/router.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
}

func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	match := rt.reURL.FindStringSubmatch(r.URL.Path)
	if len(match) == 3 {
		key := match[1][0]
		index := indexZettel
		if len(match[2]) == 0 {
			index = indexList
		}
		if mh, ok := rt.tables[index][key]; ok {
			if handler, ok := mh[r.Method]; ok {
				r.URL.Path = "/" + match[2]
				handler.ServeHTTP(w, r)
				return







|







90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
}

func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	match := rt.reURL.FindStringSubmatch(r.URL.Path)
	if len(match) == 3 {
		key := match[1][0]
		index := indexZettel
		if match[2] == "" {
			index = indexList
		}
		if mh, ok := rt.tables[index][key]; ok {
			if handler, ok := mh[r.Method]; ok {
				r.URL.Path = "/" + match[2]
				handler.ServeHTTP(w, r)
				return

Added www/build.md.























































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# How to build the Zettelstore
## Prerequisites
You must install the following software:

* A current, supported [release of Go](https://golang.org/doc/devel/release.html),
* [golint](https://github.com/golang/lint|golint),
* [Fossil](https://fossil-scm.org/).

## Clone the repository
Most of this is covered by the excellent Fossil documentation.

1. Create a directory to store your Fossil repositories.
   Let's assume, you have created <tt>$HOME/fossil</tt>.
1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossil/zettelstore.fossil`.
1. Create a working directory.
   Let's assume, you have created <tt>$HOME/zettelstore</tt>.
1. Change into this directory: `cd $HOME/zettelstore`.
1. Open development: `fossil open $HOME/fossil/zettelstore.fossil`.

(If you are not able to use Fossil, you could try the Git mirror
<https://github.com/zettelstore/zettelstore>.)

## The build tool
In directory <tt>tools</tt> there is a Go file called <tt>build.go</tt>.
It automates most aspects, (hopefully) platform-independent.

The script is called as:

```
go run tools/build.go [-v] COMMAND
```

The flag `-v` enables the verbose mode.
It outputs all commands called by the tool.

`COMMAND` is one of:

* `build`: builds the software with correct version information and places it
  into a freshly created directory <tt>bin</tt>.
* `check`: checks the current state of the working directory to be ready for
  release (or commit).
* `release`: executes `check` command and if this was successful, builds the
  software for various platforms, and creates ZIP files for each executable.
  Everything is placed in the directory <tt>releases</tt>.
* `clean`: removes the directories <tt>bin</tt> and <tt>releases</tt>.
* `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
```

In case of errors, please send the output of the verbose execution:

```
go run tools/build.go -v build
```

Changes to www/changes.wiki.

1
2















































3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<title>Change Log</title>
















































<a name="0_0_9"></a>
<h2>Changes for Version 0.0.9 (2021-01-29)</h2>
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].

<h3>Server / API</h3>
  *  (major) Support for property metadata.
             Metadata key <tt>published</tt> is the first example of such a property.
  *  (major) A background activity (called <i>indexer</i>) continuously monitors
             zettel changes to establish the reverse direction of found internal links.
             This affects the new metadata keys <tt>precursor</tt> and <tt>folge</tt>.
             A user specifies the precursor of a zettel and the indexer computes the
             property metadata for <a href="https://forum.zettelkasten.de/discussion/996/definition-folgezettel">Folgezettel</a>.
             Metadata keys with type &ldquo;Identifier&rdquo; or &ldquo;IdentifierSet&rdquo;
             that have no inverse key (like <tt>precursor</tt> and <tt>folge</tt>
             with add to the key <tt>forward</tt> that also collects all internal
             links within the content. The computed inverse is <tt>backward</tt>, which provides all backlinks.
             The key <tt>back</tt> is computed as the value of <tt>backward</tt>, but without forward links.
             Therefore, <tt>back</tt> is something like the list of &ldquo;smart backlinks&rdquo;.
  *  (minor) If Zettelstore is being stopped, an appropriate message is written in the console log.
  *  (minor) New computed zettel with enviromental data, the list of supported meta data keys,
             and statistics about all configured zettel places.
             Some other computed zettel got a new identifier (to make room for other variant).
  *  (minor) Remove zettel <tt>00000000000004</tt>, 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 <tt>00000000000006</tt> (<i>Zettelstore Environment Values</i>).
  *  (minor) Predefined templates for new zettel do not contain any value for attribute <tt>visibility</tt> any more.
  *  (minor) Add a new metadata key type called &ldquo;Zettelmarkup&rdquo;.


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>













|







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<title>Change Log</title>

<a name="0_0_11"></a>
<h2>Changes for Version 0.0.11 (pending)</h2>

<a name="0_0_10"></a>
<h2>Changes for Version 0.0.10 (2021-02-26)</h2>
  *  Menu item &ldquo;Home&rdquo; now redirects to a home zettel.
     Its default identifier is <tt>000100000000</tt>.
     The identifier can be changed with configuration key <tt>home-zettel</tt>, which supersedes key <tt>start</tt>.
     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.
     (major: api, webui)
  *  A zettel that references other zettel within first-level list items, can act
     as a &ldquo;table of contents&rdquo; zettel.
     The API endpoint <tt>/o/{ID}</tt> allows to retrieve the referenced zettel in
     the same order as they occur in the zettel.
     (major: api)
  *  The zettel &ldquo;New Menu&rdquo; with identifier <tt>00000000090000</tt> contains
     a list of all zettel that should act as a template for new zettel.
     They are listed in the WebUIs &rdquo;New&ldquo; menu.
     This is an application of the previous item.
     It supersedes the usage of a role <tt>new-template</tt> introduced in [#0_0_6|version 0.0.6].
     <b>Please update your zettel if you make use of the now deprecated feature.</b>
     (major: webui)
  *  A reference that starts with two slash characters (&ldquo;<code>//</code>&rdquo;)
     it will be interpreted relative to the value of <code>url-prefix</code>.
     For example, if <code>url-prefix</code> has the value <code>/manual/</code>,
     the reference <code>&lbrack;&lbrack;Zettel list|//h]]</code> will render as
     <code>&lt;a href="/manual/h">Zettel list&lt;/a></code>. (minor: syntax)
  *  Searching/filtering ignores the leading '#' character of tags.
     (minor: api, webui)
  *  When result of filtering 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.
     (bug: server)
  *  Runtime configuration key <tt>marker-external</tt> now defaults to
     &ldquo;&amp;#10138;&rdquo; (&ldquo;&#10138;&rdquo;). It is more beautiful
     than the previous &ldquo;&amp;#8599;&amp;#xfe0e;&rdquo;
     (&ldquo;&#8599;&#65038;&rdquo;), which also needed the additional
     &ldquo;&amp;#xfe0e;&rdquo; 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.
     (minor: infrastructure)
  *  Many smaller bug fixes and inprovements, to the software and to the documentation.

<a name="0_0_9"></a>
<h2>Changes for Version 0.0.9 (2021-01-29)</h2>
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].

<h3>Server / API</h3>
  *  (major) Support for property metadata.
             Metadata key <tt>published</tt> is the first example of such a property.
  *  (major) A background activity (called <i>indexer</i>) continuously monitors
             zettel changes to establish the reverse direction of found internal links.
             This affects the new metadata keys <tt>precursor</tt> and <tt>folge</tt>.
             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 &ldquo;Identifier&rdquo; or &ldquo;IdentifierSet&rdquo;
             that have no inverse key (like <tt>precursor</tt> and <tt>folge</tt>
             with add to the key <tt>forward</tt> that also collects all internal
             links within the content. The computed inverse is <tt>backward</tt>, which provides all backlinks.
             The key <tt>back</tt> is computed as the value of <tt>backward</tt>, but without forward links.
             Therefore, <tt>back</tt> is something like the list of &ldquo;smart backlinks&rdquo;.
  *  (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 places.
             Some other computed zettel got a new identifier (to make room for other variant).
  *  (minor) Remove zettel <tt>00000000000004</tt>, 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 <tt>00000000000006</tt> (<i>Zettelstore Environment Values</i>).
  *  (minor) Predefined templates for new zettel do not contain any value for attribute <tt>visibility</tt> any more.
  *  (minor) Add a new metadata key type called &ldquo;Zettelmarkup&rdquo;.
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
             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 &ldquo;_random&rdquo; to produce a zettel list in random order.
             <tt>_order</tt> / <tt>order</tt> are now an aliases for the query parameters <tt>_sort</tt> / <tt>sort</tt>.

<h3>WebUI</h3>
  *  (major) HTML template zettel for WebUI now use <a href="https://mustache.github.io/">Mustache</a>
             syntax instead of previously used <a href="https://golang.org/pkg/html/template/">Go template</a> syntax.
             This allows these zettel to be used, even when there is another Zettelstore implementation, in another programming language.
             Mustache is available for approx. 48 programming languages, instead of only one for Go templates.
             <b>If you modified your templates, you <i>must</i> adapt them to the new syntax.
             Otherwise the WebUI will not work.</b>
  *  (major) Show zettel identifier of folgezettel and precursor zettel in the header of a rendered zettel.
             If a zettel has backlinks, they are shown at the botton of the page
             (ldquo;Links to this zettel&rdquo;).
  *  (minor) All property metadata, even computed metadata is shown in the info page of a zettel.
  *  (minor) Rendering of metadata keys <tt>title</tt> and <tt>default-title</tt> 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.

<a name="0_0_8"></a>







|
|






|







86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
             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 &ldquo;_random&rdquo; to produce a zettel list in random order.
             <tt>_order</tt> / <tt>order</tt> are now an aliases for the query parameters <tt>_sort</tt> / <tt>sort</tt>.

<h3>WebUI</h3>
  *  (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 template] syntax.
             This allows these zettel to be used, even when there is another Zettelstore implementation, in another programming language.
             Mustache is available for approx. 48 programming languages, instead of only one for Go templates.
             <b>If you modified your templates, you <i>must</i> adapt them to the new syntax.
             Otherwise the WebUI will not work.</b>
  *  (major) Show zettel identifier of folgezettel and precursor zettel in the header of a rendered zettel.
             If a zettel has backlinks, they are shown at the botton of the page
             (&ldquo;Links to this zettel&rdquo;).
  *  (minor) All property metadata, even computed metadata is shown in the info page of a zettel.
  *  (minor) Rendering of metadata keys <tt>title</tt> and <tt>default-title</tt> 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.

<a name="0_0_8"></a>
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<h3>WebUI</h3>
  *  (minor) Remove list of tags in &ldquo;List Zettel&rdquo; and search results.
             There was some feedback that the additional tags were not helpful.
  *  (minor) Move zettel field "role" above "tags" and move "syntax" more to "content".
  *  (minor) Rename zettel operation &ldquo;clone&rdquo; to &ldquo;copy&rdquo;.
  *  (major) All predefined HTML templates have now a visibility value &ldquo;expert&rdquo;.
             If you want to see them as an non-expert owner, you must temporary enable <tt>expert-mode</tt> and change the <tt>visibility</tt> metadata value.
  *  (minor) Initial support for <a href="https://zettelkasten.de/posts/tags/folgezettel/">Folgezettel</a>.
             If you click on &ldquo;Folge&rdquo; (detail view or info view), a new zettel is created with a reference (<tt>precursor</tt>) to the original zettel.
             Title, role, tags, and syntax are copied from the original zettel.
  *  (major) Most predefined zettel have a title prefix of &ldquo;Zettelstore&rdquo;.
  *  (minor) If started in simple mode, e.g. via double click or without any command, some information for the new user is presented.
             In the terminal, there is a hint about opening the web browser and use a specific URL.
             A <i>Welcome zettel</i> is created, to give some more information.
             (This change also applies to the server itself, but it is more suited to the WebUI user.)

<a name="0_0_7"></a>
<h2>Changes for Version 0.0.7 (2020-11-24)</h2>
  *  With this version, Zettelstore and this manual got a new license, the
     <a href="https://joinup.ec.europa.eu/collection/eupl">European Union Public Licence</a> (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.

<a name="0_0_6"></a>
<h2>Changes for Version 0.0.6 (2020-11-23)</h2>
<h3>Server</h3>







|











|







152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
<h3>WebUI</h3>
  *  (minor) Remove list of tags in &ldquo;List Zettel&rdquo; and search results.
             There was some feedback that the additional tags were not helpful.
  *  (minor) Move zettel field "role" above "tags" and move "syntax" more to "content".
  *  (minor) Rename zettel operation &ldquo;clone&rdquo; to &ldquo;copy&rdquo;.
  *  (major) All predefined HTML templates have now a visibility value &ldquo;expert&rdquo;.
             If you want to see them as an non-expert owner, you must temporary enable <tt>expert-mode</tt> and change the <tt>visibility</tt> metadata value.
  *  (minor) Initial support for [https://zettelkasten.de/posts/tags/folgezettel/|Folgezettel].
             If you click on &ldquo;Folge&rdquo; (detail view or info view), a new zettel is created with a reference (<tt>precursor</tt>) to the original zettel.
             Title, role, tags, and syntax are copied from the original zettel.
  *  (major) Most predefined zettel have a title prefix of &ldquo;Zettelstore&rdquo;.
  *  (minor) If started in simple mode, e.g. via double click or without any command, some information for the new user is presented.
             In the terminal, there is a hint about opening the web browser and use a specific URL.
             A <i>Welcome zettel</i> is created, to give some more information.
             (This change also applies to the server itself, but it is more suited to the WebUI user.)

<a name="0_0_7"></a>
<h2>Changes for Version 0.0.7 (2020-11-24)</h2>
  *  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.

<a name="0_0_6"></a>
<h2>Changes for Version 0.0.6 (2020-11-23)</h2>
<h3>Server</h3>
172
173
174
175
176
177
178

179
180
181
182
183
184
185
             The WebUI menu item &ldquo;New&rdquo; changed to a drop-down list with all those zettel, ordered by their identifier.
             All metadata keys with the prefix <tt>new-</tt> 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 &ldquo;New Zettel&rdquo; has now a different zettel identifier
             (now: <tt>00000000091001</tt>, was: <tt>00000000040001</tt>).
             <b>Please update it, if you changed that zettel.</b>

  *  (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 <i>Zettelstore Runtime Configuration</i> key <tt>list-page-size</tt> 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 <code>line-height</code> a little smaller (previous: 1.6, now 1.4) and move list items to the left.








>







219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
             The WebUI menu item &ldquo;New&rdquo; changed to a drop-down list with all those zettel, ordered by their identifier.
             All metadata keys with the prefix <tt>new-</tt> 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 &ldquo;New Zettel&rdquo; has now a different zettel identifier
             (now: <tt>00000000091001</tt>, was: <tt>00000000040001</tt>).
             <b>Please update it, if you changed that zettel.</b>
             <br>Note: this feature was superseded in [#0_0_10|version 0.0.10] by the &ldquo;New Menu&rdquo; 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 <i>Zettelstore Runtime Configuration</i> key <tt>list-page-size</tt> 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 <code>line-height</code> a little smaller (previous: 1.6, now 1.4) and move list items to the left.

Changes to www/download.wiki.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

18
19
20
21
22
23
<title>Download</title>
<h1>Download of Zettelstore Software</h1>
<h2>Foreword</h2>
  *  Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later.
  *  The software is provided as-is.
  *  There is no guarantee that it will not damage your system.
  *  However, it is in use by the main developer since March 2020 without any damage.
  *  It may be useful for you. It is useful for me.
  *  Take a look at the <a href="/manual/">manual</a> to know how to start and use it.

<h2>ZIP-ped Executables</h2>
Build: <code>v0.0.9</code> (2021-01-29).

  *  [/uv/zettelstore.zip|Linux] (amd64)
  *  [/uv/zettelstore-arm6.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore.exe.zip|Windows] (amd64)
  *  [/uv/iZettelstore.zip|macOs] (amd64)


Unzip the appropriate file, install and execute Zettelstore according to the manual.

<h2>Zettel for the manual</h2>
As a starter, you can download the zettel for the manual [/uv/manual.zip|here].
Just unzip the file and put it into your zettel folder.








|


|

|
|
|
|
>




|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<title>Download</title>
<h1>Download of Zettelstore Software</h1>
<h2>Foreword</h2>
  *  Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later.
  *  The software is provided as-is.
  *  There is no guarantee that it will not damage your system.
  *  However, it is in use by the main developer since March 2020 without any damage.
  *  It may be useful for you. It is useful for me.
  *  Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it.

<h2>ZIP-ped Executables</h2>
Build: <code>v0.0.10</code> (2021-02-26).

  *  [/uv/zettelstore-0.0.10-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.0.10-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.0.10-windows-amd64.zip|Windows] (amd64)
  *  [/uv/zettelstore-0.0.10-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.0.10-darwin-arm64.zip|macOS] (arm64, aka Apple silicon)

Unzip the appropriate file, install and execute Zettelstore according to the manual.

<h2>Zettel for the manual</h2>
As a starter, you can download the zettel for the manual [/uv/manual-0.0.10.zip|here].
Just unzip the contained files and put them into your zettel folder.

Changes to www/impri.wiki.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<title>Imprint & Privacy</title>
<h1>Imprint</h1>
<p>
Detlef Stern<br>
Max-Planck-Str. 39<br>
74081 Heilbronn<br>
Phone: +49 (173) 4905619<br>
Mail: ds (at) zettelstore.de
</p>

<h1>Privacy</h1>
<p>
If you do not log into this site, or login as the user &quot;anonymous&quot;,
the only personal data this web service will process is your IP adress. It will
be used to send the data of the website you requested to you and to mitigate
possible attacks against this website.
</p>
<p>
This website is hosted by <a href="https://ionos.de">1&amp;1 IONOS SE</a>.
According to
<a href="https://www.ionos.de/hilfe/datenschutz/datenverarbeitung-von-webseitenbesuchern-ihres-11-ionos-produktes/andere-11-ionos-produkte/">their information</a>,
no processing of personal data is done by them.
</p>


<



|

<


<




|
<
|

|

<
1
2

3
4
5
6
7

8
9

10
11
12
13
14

15
16
17
18

<title>Imprint & Privacy</title>
<h1>Imprint</h1>

Detlef Stern<br>
Max-Planck-Str. 39<br>
74081 Heilbronn<br>
Phone: +49 (15678) 386566<br>
Mail: ds (at) zettelstore.de


<h1>Privacy</h1>

If you do not log into this site, or login as the user &quot;anonymous&quot;,
the only personal data this web service will process is your IP adress. It will
be used to send the data of the website you requested to you and to mitigate
possible attacks against this website.


This website is hosted by [https://ionos.de|1&amp;1 IONOS SE].
According to
[https://www.ionos.de/hilfe/datenschutz/datenverarbeitung-von-webseitenbesuchern-ihres-11-ionos-produktes/andere-11-ionos-produkte/|their information],
no processing of personal data is done by them.

Changes to www/index.wiki.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22




23








24
25
26
<title>Home</title>

<b>Zettelstore</b> is a software that collects and relates your notes
(&ldquo;zettel&rdquo;) to represent and enhance your knowledge. It helps with
many tasks of personal knowledge management by explicitly supporting the
[href="https://en.wikipedia.org/wiki/Zettelkasten|Zettelkasten method]. The
method is based on creating many individual notes, each with one idea or
information, that are related to each other. Since knowledge is typically build
up gradually, one major focus is a long-term store of these notes, hence the
name &ldquo;Zettelstore&rdquo;.

To get an initial impression, take a look at the <a href="/manual/">manual</a>.
It is a live example of the 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://twitter.com/zettelstore|Stay tuned]&hellip;
<hr>
<h3>Latest Release: 0.0.9 (2021-01-29)</h3>
  *  [./download.wiki|Download]
  *  [./changes.wiki#0_0_9|Change Summary]




  *  [./plan.wiki|Limitations and Planned Improvements]








  *  [/dir?ci=trunk|Source Code] (mirrored on <a href="https://github.com/zettelstore/zettelstore">GitHub</a>)
  *  [/download|Download the source code] as a Tarball or a ZIP file
     (you must [/login|login] as user &quot;anonymous&quot;).





|





|







|

|
>
>
>
>
|
>
>
>
>
>
>
>
>
|


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<title>Home</title>

<b>Zettelstore</b> is a software that collects and relates your notes
(&ldquo;zettel&rdquo;) to represent and enhance your knowledge. It helps with
many tasks of personal knowledge management by explicitly supporting the
[https://en.wikipedia.org/wiki/Zettelkasten|Zettelkasten method]. The
method is based on creating many individual notes, each with one idea or
information, that are related to each other. Since knowledge is typically build
up gradually, one major focus is a long-term store of these notes, hence the
name &ldquo;Zettelstore&rdquo;.

To get an initial impression, take a look at the [https://zettelstore.de/manual/|manual].
It is a live example of the 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://twitter.com/zettelstore|Stay tuned]&hellip;
<hr>
<h3>Latest Release: 0.0.10 (2021-02-26)</h3>
  *  [./download.wiki|Download]
  *  [./changes.wiki#0_0_10|Change Summary]
  *  [/timeline?p=version-0.0.10&bt=version-0.0.9&y=ci|Check-ins for version 0.0.10],
     [/vdiff?to=version-0.0.10&from=version-0.0.9|content diff]
  *  [/timeline?df=version-0.0.10&y=ci|Check-ins derived from the 0.0.10 release],
     [/vdiff?from=version-0.0.10&to=trunk|content diff]
  *  [./plan.wiki|Limitations and planned Improvements]
  *  [/timeline?t=release|Timeline of all past releases]

<hr>
<h2>Build instructions</h2>
Just install [https://fossil-scm.org|Fossil],
[https://golang.org/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 &quot;anonymous&quot;).

Changes to www/plan.wiki.

1
2
3
4
5
6
7
8
<title>Limitations and Planned Improvements</title>

Here is a list of some shortcomings of Zettelstore.
They are planned to be solved.

<h3>Serious limitations</h3>
  *  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
|







1
2
3
4
5
6
7
8
<title>Limitations and planned Improvements</title>

Here is a list of some shortcomings of Zettelstore.
They are planned to be solved.

<h3>Serious limitations</h3>
  *  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
21
22
23
24
25
26
27
28
29
30
  *  The horizontal tab character (<tt>U+0009</tt>) is not supported.
  *  Missing support for citation keys.
  *  &hellip;

<h3>Planned improvements</h3>
  *  Support for mathematical content is missing, e.g. <code>$$F(x) &=
     \\int^a_b \\frac{1}{3}x^3$$</code>.
  *  Render zettel in <a href="https://pandoc.org">pandoc's</a> JSON version of
     their native AST to make pandoc an external renderer for Zettelstore.
  *  &hellip;







|


21
22
23
24
25
26
27
28
29
30
  *  The horizontal tab character (<tt>U+0009</tt>) is not supported.
  *  Missing support for citation keys.
  *  &hellip;

<h3>Planned improvements</h3>
  *  Support for mathematical content is missing, e.g. <code>$$F(x) &=
     \\int^a_b \\frac{1}{3}x^3$$</code>.
  *  Render zettel in [https://pandoc.org|pandoc's] JSON version of
     their native AST to make pandoc an external renderer for Zettelstore.
  *  &hellip;